add journal application module

This commit is contained in:
Jörg Prante 2023-08-29 13:38:59 +02:00
parent e3adbd4369
commit 582e7dd895
13 changed files with 354 additions and 24 deletions

View file

@ -1,5 +1,5 @@
group = org.xbib
name = net-http
version = 3.6.2
version = 3.7.0
org.gradle.warning.mode = ALL

View file

@ -0,0 +1,3 @@
dependencies {
api project(':net-http-server')
}

View file

@ -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;
}

View file

@ -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<StampedEntry> 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;
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -54,6 +54,8 @@ public class BaseApplication implements Application {
protected List<ApplicationModule> 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");
}

View file

@ -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) {
}
}

View file

@ -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<HttpService> 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);
}

View file

@ -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() {

View file

@ -49,7 +49,9 @@ public interface HttpRouterContext {
void reset();
void fail();
void fail(Throwable throwable);
Throwable getFail();
boolean isFailed();

View file

@ -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) {

View file

@ -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'