From 582e7dd89529f72699ed75b58241811f26e0b3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Prante?= Date: Tue, 29 Aug 2023 13:38:59 +0200 Subject: [PATCH] add journal application module --- gradle.properties | 2 +- .../build.gradle | 3 + .../src/main/java/module-info.java | 7 + .../server/application/journal/Journal.java | 168 ++++++++++++++++++ .../journal/JournalApplicationModule.java | 70 ++++++++ .../server/application/ApplicationModule.java | 16 +- .../server/application/BaseApplication.java | 28 ++- .../application/BaseApplicationModule.java | 26 ++- .../net/http/server/route/BaseHttpRouter.java | 29 ++- .../server/route/BaseHttpRouterContext.java | 13 +- .../http/server/route/HttpRouterContext.java | 4 +- .../GroovyTemplateApplicationModule.java | 11 +- settings.gradle | 1 + 13 files changed, 354 insertions(+), 24 deletions(-) create mode 100644 net-http-server-application-journal/build.gradle create mode 100644 net-http-server-application-journal/src/main/java/module-info.java create mode 100644 net-http-server-application-journal/src/main/java/org/xbib/net/http/server/application/journal/Journal.java create mode 100644 net-http-server-application-journal/src/main/java/org/xbib/net/http/server/application/journal/JournalApplicationModule.java diff --git a/gradle.properties b/gradle.properties index c5bb41f..b77a64a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group = org.xbib name = net-http -version = 3.6.2 +version = 3.7.0 org.gradle.warning.mode = ALL diff --git a/net-http-server-application-journal/build.gradle b/net-http-server-application-journal/build.gradle new file mode 100644 index 0000000..ce6e8b7 --- /dev/null +++ b/net-http-server-application-journal/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':net-http-server') +} diff --git a/net-http-server-application-journal/src/main/java/module-info.java b/net-http-server-application-journal/src/main/java/module-info.java new file mode 100644 index 0000000..92d15b8 --- /dev/null +++ b/net-http-server-application-journal/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module org.xbib.net.http.server.application.journal { + requires org.xbib.net; + requires org.xbib.net.http; + requires org.xbib.net.http.server; + requires org.xbib.settings.api; + requires java.logging; +} diff --git a/net-http-server-application-journal/src/main/java/org/xbib/net/http/server/application/journal/Journal.java b/net-http-server-application-journal/src/main/java/org/xbib/net/http/server/application/journal/Journal.java new file mode 100644 index 0000000..40ce0b8 --- /dev/null +++ b/net-http-server-application-journal/src/main/java/org/xbib/net/http/server/application/journal/Journal.java @@ -0,0 +1,168 @@ +package org.xbib.net.http.server.application.journal; + +import org.xbib.net.util.ExceptionFormatter; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.util.EnumSet; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Journal { + + private static final Logger logger = Logger.getLogger(Journal.class.getName()); + + private final Path journalPath; + + private final ReentrantReadWriteLock lock; + + public Journal(String journalPathName) throws IOException { + this.journalPath = createJournal(journalPathName); + this.lock = new ReentrantReadWriteLock(); + } + + private static Path createJournal(String logPathName) throws IOException { + Path logPath = Paths.get(logPathName); + Files.createDirectories(logPath); + if (!Files.exists(logPath) || !Files.isWritable(logPath)) { + throw new IOException("unable to write to log path = " + logPath); + } + return logPath; + } + + public void logRequest(String stamp, String request) throws IOException { + logger.log(Level.FINE, stamp + " request = " + request); + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + writeLock.lock(); + try (OutputStream outputStream = Files.newOutputStream(journalPath.resolve(stamp + ".log"), StandardOpenOption.CREATE)) { + outputStream.write(request.getBytes(StandardCharsets.UTF_8)); + } finally { + writeLock.unlock(); + } + } + + public void logSuccess(String stamp, String response) throws IOException { + logger.log(Level.FINE, stamp + " response = " + response); + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + writeLock.lock(); + Path path = journalPath.resolve("success").resolve(stamp + ".request"); + Files.createDirectories(path.getParent()); + Files.move(journalPath.resolve(stamp), path); + try (OutputStream outputStream = Files.newOutputStream(journalPath.resolve("success").resolve(stamp + ".response"), StandardOpenOption.CREATE)) { + outputStream.write(response.getBytes(StandardCharsets.UTF_8)); + } finally { + writeLock.unlock(); + } + } + + public void logFail(String stamp, Throwable t) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + writeLock.lock(); + Path path = journalPath.resolve("fail").resolve(stamp + ".request"); + Files.createDirectories(path.getParent()); + Files.move(journalPath.resolve(stamp), path); + // save throwable in extra file + try (OutputStream outputStream = Files.newOutputStream(journalPath.resolve("fail").resolve(stamp + ".exception"), StandardOpenOption.CREATE)) { + outputStream.write(ExceptionFormatter.format(t).getBytes(StandardCharsets.UTF_8)); + } finally { + writeLock.unlock(); + } + } + + public void retry(Consumer consumer) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + writeLock.lock(); + PathMatcher pathMatcher = journalPath.getFileSystem().getPathMatcher("glob:*.log"); + try { + Files.walkFileTree(journalPath, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path p, BasicFileAttributes a) throws IOException { + if ((Files.isRegularFile(p) && pathMatcher.matches(p.getFileName()))) { + String stamp = p.getFileName().toString(); + String entry = Files.readString(p); + consumer.accept(new StampedEntry(stamp, entry)); + Files.delete(p); + } + return FileVisitResult.CONTINUE; + } + }); + } finally { + writeLock.unlock(); + } + } + + public void purgeSuccess(Instant instant) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + writeLock.lock(); + PathMatcher pathMatcher = journalPath.getFileSystem().getPathMatcher("glob:*.request"); + try { + Files.walkFileTree(journalPath.resolve("success"), EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path p, BasicFileAttributes a) throws IOException { + if ((Files.isRegularFile(p) && pathMatcher.matches(p.getFileName()))) { + if (Files.getLastModifiedTime(p).toInstant().isBefore(instant)) { + Files.delete(p); + } + } + return FileVisitResult.CONTINUE; + } + }); + } finally { + writeLock.unlock(); + } + } + + public void purgeFail(Instant instant) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + writeLock.lock(); + PathMatcher pathMatcher = journalPath.getFileSystem().getPathMatcher("glob:*.request"); + try { + Files.walkFileTree(journalPath.resolve("fail"), EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path p, BasicFileAttributes a) throws IOException { + if ((Files.isRegularFile(p) && pathMatcher.matches(p.getFileName()))) { + if (Files.getLastModifiedTime(p).toInstant().isBefore(instant)) { + Files.delete(p); + } + } + return FileVisitResult.CONTINUE; + } + }); + } finally { + writeLock.unlock(); + } + } + + public static class StampedEntry { + + private final String stamp; + + private final String entry; + + public StampedEntry(String stamp, String entry) { + this.stamp = stamp; + this.entry = entry; + } + + public String getStamp() { + return stamp; + } + + public String getEntry() { + return entry; + } + } +} diff --git a/net-http-server-application-journal/src/main/java/org/xbib/net/http/server/application/journal/JournalApplicationModule.java b/net-http-server-application-journal/src/main/java/org/xbib/net/http/server/application/journal/JournalApplicationModule.java new file mode 100644 index 0000000..83781fb --- /dev/null +++ b/net-http-server-application-journal/src/main/java/org/xbib/net/http/server/application/journal/JournalApplicationModule.java @@ -0,0 +1,70 @@ +package org.xbib.net.http.server.application.journal; + +import org.xbib.net.http.server.HttpRequest; +import org.xbib.net.http.server.HttpResponseBuilder; +import org.xbib.net.http.server.application.Application; +import org.xbib.net.http.server.application.BaseApplicationModule; +import org.xbib.net.http.server.route.HttpRouterContext; +import org.xbib.net.http.server.service.HttpService; +import org.xbib.settings.Settings; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class JournalApplicationModule extends BaseApplicationModule { + + private static final Logger logger = Logger.getLogger(JournalApplicationModule.class.getName()); + + private final Journal journal; + + public JournalApplicationModule(Application application, String name, Settings settings) throws IOException { + super(application, name, settings); + this.journal = new Journal(settings.get("application.journal", "/var/tmp/application/journal")); + } + + @Override + public void onOpen(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest) { + String stamp = createStamp(httpRequest); + httpRouterContext.getAttributes().put("_stamp", stamp); + try { + journal.logRequest(stamp, httpRequest.asJson()); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + + @Override + public void onSuccess(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest) { + String stamp = httpRouterContext.getAttributes().get(String.class, "_stamp"); + HttpResponseBuilder httpResponseBuilder = httpRouterContext.getAttributes().get(HttpResponseBuilder.class, "response"); + if (stamp != null && httpResponseBuilder != null) { + try { + journal.logSuccess(stamp, httpResponseBuilder.getResponseStatus().codeAsText()); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + } + + @Override + public void onFail(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest, Throwable throwable) { + String stamp = httpRouterContext.getAttributes().get(String.class, "_stamp"); + if (stamp != null) { + try { + journal.logFail(stamp, throwable); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + } + + private String createStamp(HttpRequest httpRequest) { + return httpRequest.getLocalAddress().getAddress().getHostAddress() + "_" + + httpRequest.getLocalAddress().getPort() + "_" + + httpRequest.getRemoteAddress().getAddress().getHostAddress() + "_" + + httpRequest.getRemoteAddress().getPort() + "_" + + LocalDateTime.now(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/application/ApplicationModule.java b/net-http-server/src/main/java/org/xbib/net/http/server/application/ApplicationModule.java index 8888183..8c7c3be 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/application/ApplicationModule.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/application/ApplicationModule.java @@ -11,13 +11,23 @@ public interface ApplicationModule { void onOpen(HttpRouterContext httpRouterContext); + void onSuccess(HttpRouterContext httpRouterContext); + + void onFail(HttpRouterContext httpRouterContext, Throwable throwable); + void onOpen(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest); - void onClose(HttpRouterContext httpRouterContext); + void onSuccess(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest); + + void onFail(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest, Throwable throwable); void onOpen(Session session); - void onClose(Session session); + void onSuccess(Session session); - void onClose(); + void onFail(Session session, Throwable throwable); + + void onSuccess(); + + void onFail(Throwable throwable); } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java b/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java index 0c8b505..f5b5d77 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java @@ -54,6 +54,8 @@ public class BaseApplication implements Application { protected List applicationModuleList; + private Throwable throwable; + protected BaseApplication(BaseApplicationBuilder builder) { this.builder = builder; this.sessionName = builder.settings.get("session.name", "SESS"); @@ -240,26 +242,38 @@ public class BaseApplication implements Application { @Override public void onDestroy(Session session) { logger.log(Level.FINER, "session name = " + sessionName + " destroyed = " + session); - applicationModuleList.forEach(module -> module.onClose(session)); + if (throwable != null) { + applicationModuleList.forEach(module -> module.onFail(session, throwable)); + } else { + applicationModuleList.forEach(module -> module.onSuccess(session)); + } } @Override public void onOpen(HttpRouterContext httpRouterContext) { + this.throwable = null; try { // call modules after request/cookie/session setup applicationModuleList.forEach(module -> module.onOpen(httpRouterContext)); } catch (Throwable t) { + this.throwable = t; + applicationModuleList.forEach(module -> module.onFail(httpRouterContext, t)); builder.httpRouter.routeToErrorHandler(httpRouterContext, t); - httpRouterContext.fail(); + httpRouterContext.fail(t); } } @Override public void onClose(HttpRouterContext httpRouterContext) { try { - // call modules before session/cookie - applicationModuleList.forEach(module -> module.onClose(httpRouterContext)); + if (throwable != null) { + applicationModuleList.forEach(module -> module.onFail(httpRouterContext, throwable)); + } else { + applicationModuleList.forEach(module -> module.onSuccess(httpRouterContext)); + } } catch (Throwable t) { + this.throwable = t; + applicationModuleList.forEach(module -> module.onFail(httpRouterContext, t)); builder.httpRouter.routeToErrorHandler(httpRouterContext, t); } finally { try { @@ -307,7 +321,11 @@ public class BaseApplication implements Application { // stop dispatching and stop dispatched requests applicationModuleList.forEach(module -> { logger.log(Level.FINE, "application closing module " + module); - module.onClose(); + if (throwable != null) { + module.onFail(throwable); + } else { + module.onSuccess(); + } }); logger.log(Level.INFO, "application closed"); } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplicationModule.java b/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplicationModule.java index 55fb15e..ae055f9 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplicationModule.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplicationModule.java @@ -29,12 +29,24 @@ public abstract class BaseApplicationModule implements ApplicationModule { public void onOpen(HttpRouterContext httpRouterContext) { } + @Override + public void onSuccess(HttpRouterContext httpRouterContext) { + } + + @Override + public void onFail(HttpRouterContext httpRouterContext, Throwable throwable) { + } + @Override public void onOpen(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest) { } @Override - public void onClose(HttpRouterContext httpRouterContext) { + public void onSuccess(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest) { + } + + @Override + public void onFail(HttpRouterContext httpRouterContext, HttpService httpService, HttpRequest httpRequest, Throwable throwable) { } @Override @@ -42,10 +54,18 @@ public abstract class BaseApplicationModule implements ApplicationModule { } @Override - public void onClose(Session session) { + public void onSuccess(Session session) { } @Override - public void onClose() { + public void onFail(Session session, Throwable throwable) { + } + + @Override + public void onSuccess() { + } + + @Override + public void onFail(Throwable throwable) { } } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java index 5b9b2c3..2942bb6 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java @@ -36,6 +36,7 @@ import org.xbib.net.http.server.HttpRequest; import org.xbib.net.http.server.HttpRequestBuilder; import org.xbib.net.http.server.HttpResponseBuilder; import org.xbib.net.http.server.application.Application; +import org.xbib.net.http.server.application.ApplicationModule; import org.xbib.net.http.server.domain.HttpDomain; import org.xbib.net.http.server.handler.InternalServerErrorHandler; import org.xbib.net.http.server.service.HttpService; @@ -140,15 +141,19 @@ public class BaseHttpRouter implements HttpRouter { return; } for (HttpRouteResolver.Result httpRouteResolverResult : httpRouteResolverResults) { + HttpService httpService = null; + HttpRequest httpRequest = null; try { // first: create the final request setResolverResult(httpRouterContext, httpRouteResolverResult); - HttpService httpService = httpRouteResolverResult.getValue(); - HttpRequest httpRequest = httpRouterContext.getRequest(); - application.getModules().forEach(module -> module.onOpen(httpRouterContext, httpService, httpRequest)); + httpService = httpRouteResolverResult.getValue(); + httpRequest = httpRouterContext.getRequest(); + for (ApplicationModule module : application.getModules()) { + module.onOpen(httpRouterContext, httpService, httpRequest); + } // second: security check, authentication etc. if (httpService.getSecurityDomain() != null) { - logger.log(Level.FINEST, () -> "handling security domain service " + httpService); + logger.log(Level.FINEST, "handling security domain service " + httpService); for (HttpHandler httpHandler : httpService.getSecurityDomain().getHandlers()) { logger.log(Level.FINEST, () -> "handling security domain handler " + httpHandler); httpHandler.handle(httpRouterContext); @@ -159,14 +164,26 @@ public class BaseHttpRouter implements HttpRouter { } // after security checks, accept service, open and execute service httpRouterContext.getAttributes().put("service", httpService); - logger.log(Level.FINEST, () -> "handling service " + httpService); + logger.log(Level.FINEST, "handling service " + httpService); httpService.handle(httpRouterContext); // if service signals that work is done, break if (httpRouterContext.isDone() || httpRouterContext.isFailed()) { + for (ApplicationModule module : application.getModules()) { + module.onSuccess(httpRouterContext, httpService, httpRequest); + } + break; + } + if (httpRouterContext.isFailed()) { + for (ApplicationModule module : application.getModules()) { + module.onFail(httpRouterContext, httpService, httpRequest, httpRouterContext.getFail()); + } break; } } catch (HttpException e) { logger.log(Level.SEVERE, e.getMessage(), e); + for (ApplicationModule module : application.getModules()) { + module.onFail(httpRouterContext, httpService, httpRequest, httpRouterContext.getFail()); + } routeException(e); break; } catch (Throwable t) { @@ -330,7 +347,7 @@ public class BaseHttpRouter implements HttpRouter { @Override public void routeToErrorHandler(HttpRouterContext httpRouterContext, Throwable t) { httpRouterContext.getAttributes().put("_throwable", t); - httpRouterContext.fail(); + httpRouterContext.fail(t); routeStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, httpRouterContext); } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterContext.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterContext.java index 3642a5e..0c81781 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterContext.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterContext.java @@ -53,7 +53,7 @@ public class BaseHttpRouterContext implements HttpRouterContext { private boolean done; - private boolean failed; + private Throwable throwable; private boolean next; @@ -201,12 +201,17 @@ public class BaseHttpRouterContext implements HttpRouterContext { @Override public boolean isFailed() { - return failed; + return throwable != null; } @Override - public void fail() { - this.failed = true; + public void fail(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public Throwable getFail() { + return throwable; } public void next() { diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouterContext.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouterContext.java index bb8ccad..ec97cbb 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouterContext.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/HttpRouterContext.java @@ -49,7 +49,9 @@ public interface HttpRouterContext { void reset(); - void fail(); + void fail(Throwable throwable); + + Throwable getFail(); boolean isFailed(); diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateApplicationModule.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateApplicationModule.java index c5b50c6..fcef4e1 100644 --- a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateApplicationModule.java +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateApplicationModule.java @@ -71,7 +71,16 @@ public class GroovyTemplateApplicationModule extends BaseApplicationModule { } @Override - public void onClose(HttpRouterContext httpRouterContext) { + public void onSuccess(HttpRouterContext httpRouterContext) { + try { + groovyTemplateRenderer.handle(httpRouterContext); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void onFail(HttpRouterContext httpRouterContext, Throwable throwable) { try { groovyTemplateRenderer.handle(httpRouterContext); } catch (IOException e) { diff --git a/settings.gradle b/settings.gradle index 822de8a..c2311c7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -68,3 +68,4 @@ include 'net-http-j2html' include 'net-http-server-application-web' include 'net-http-server-application-config' include 'net-http-server-application-database' +include 'net-http-server-application-journal'