add journal application module
This commit is contained in:
parent
e3adbd4369
commit
582e7dd895
13 changed files with 354 additions and 24 deletions
|
@ -1,5 +1,5 @@
|
|||
group = org.xbib
|
||||
name = net-http
|
||||
version = 3.6.2
|
||||
version = 3.7.0
|
||||
|
||||
org.gradle.warning.mode = ALL
|
||||
|
|
3
net-http-server-application-journal/build.gradle
Normal file
3
net-http-server-application-journal/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
api project(':net-http-server')
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -49,7 +49,9 @@ public interface HttpRouterContext {
|
|||
|
||||
void reset();
|
||||
|
||||
void fail();
|
||||
void fail(Throwable throwable);
|
||||
|
||||
Throwable getFail();
|
||||
|
||||
boolean isFailed();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue