diff --git a/http-netty/src/main/java/io/micronaut/http/netty/channel/ChannelPipelineCustomizer.java b/http-netty/src/main/java/io/micronaut/http/netty/channel/ChannelPipelineCustomizer.java index e89428ed925..bed00ebd4a1 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/channel/ChannelPipelineCustomizer.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/channel/ChannelPipelineCustomizer.java @@ -53,6 +53,7 @@ public interface ChannelPipelineCustomizer { String HANDLER_HTTP2_PROTOCOL_NEGOTIATOR = "http2-protocol-negotiator"; String HANDLER_WEBSOCKET_UPGRADE = "websocket-upgrade-handler"; String HANDLER_MICRONAUT_INBOUND = "micronaut-inbound-handler"; + String HANDLER_ACCESS_LOGGER = "http-access-logger"; /** * @return Is this customizer the client. diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java index 5aaed23839e..cc69717d317 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpServer.java @@ -43,6 +43,7 @@ import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration; import io.micronaut.http.server.netty.decoders.HttpRequestDecoder; import io.micronaut.http.server.netty.encoders.HttpResponseEncoder; +import io.micronaut.http.server.netty.handler.accesslog.HttpAccessLogHandler; import io.micronaut.http.server.netty.ssl.HttpRequestCertificateHandler; import io.micronaut.http.server.netty.ssl.ServerSslBuilder; import io.micronaut.http.server.netty.types.NettyCustomizableResponseTypeHandlerRegistry; @@ -603,7 +604,6 @@ private HttpToHttp2ConnectionHandler newHttpToHttp2ConnectionHandler() { serverConfiguration.isValidateHeaders(), true ); - final HttpToHttp2ConnectionHandlerBuilder builder = new HttpToHttp2ConnectionHandlerBuilder() .frameListener(http2ToHttpAdapter); @@ -630,7 +630,9 @@ public void doOnConnect(@NonNull ChannelPipelineListener listener) { @ChannelHandler.Sharable private final class Http2OrHttpHandler extends ApplicationProtocolNegotiationHandler { private final boolean useSsl; - // both are Sharable + // all are Sharable + final HttpAccessLogHandler accessLogHandler = serverConfiguration.getAccessLogger() != null && serverConfiguration.getAccessLogger().isEnabled() ? + new HttpAccessLogHandler(serverConfiguration.getAccessLogger().getLoggerName(), serverConfiguration.getAccessLogger().getLogFormat()) : null; final HttpRequestDecoder requestDecoder = new HttpRequestDecoder(NettyHttpServer.this, environment, serverConfiguration); final HttpResponseEncoder responseDecoder = new HttpResponseEncoder(mediaTypeCodecRegistry, serverConfiguration); @@ -688,11 +690,17 @@ private Map getHandlerForProtocol(@Nullable String proto } if (protocol == null) { handlers.put(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); + if (accessLogHandler != null) { + handlers.put(HANDLER_ACCESS_LOGGER, accessLogHandler); + } } else if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { final HttpToHttp2ConnectionHandler httpToHttp2ConnectionHandler = newHttpToHttp2ConnectionHandler(); handlers.put(HANDLER_HTTP2_CONNECTION, httpToHttp2ConnectionHandler); registerMicronautChannelHandlers(handlers); handlers.put(ChannelPipelineCustomizer.HANDLER_FLOW_CONTROL, new FlowControlHandler()); + if (accessLogHandler != null) { + handlers.put(HANDLER_ACCESS_LOGGER, accessLogHandler); + } } else { handlers.put(HANDLER_HTTP_SERVER_CODEC, new HttpServerCodec( serverConfiguration.getMaxInitialLineLength(), @@ -701,13 +709,15 @@ private Map getHandlerForProtocol(@Nullable String proto serverConfiguration.isValidateHeaders(), serverConfiguration.getInitialBufferSize() )); + if (accessLogHandler != null) { + handlers.put(HANDLER_ACCESS_LOGGER, accessLogHandler); + } registerMicronautChannelHandlers(handlers); handlers.put(HANDLER_FLOW_CONTROL, new FlowControlHandler()); handlers.put(HANDLER_HTTP_KEEP_ALIVE, new HttpServerKeepAliveHandler()); handlers.put(HANDLER_HTTP_COMPRESSOR, new SmartHttpContentCompressor(httpCompressionStrategy)); handlers.put(HANDLER_HTTP_DECOMPRESSOR, new HttpContentDecompressor()); } - handlers.put(HANDLER_HTTP_STREAM, new HttpStreamsServerHandler()); handlers.put(HANDLER_HTTP_CHUNK, new ChunkedWriteHandler()); handlers.put(HttpRequestDecoder.ID, requestDecoder); @@ -787,7 +797,6 @@ public void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest) new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, connectionHandler); pipeline.addLast(cleartextHttp2ServerUpgradeHandler); - pipeline.addLast(fallbackHandlerName, new SimpleChannelInboundHandler() { @Override protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java index 23f11a9ab63..7710cbb446c 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java @@ -114,6 +114,7 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { private int compressionLevel = DEFAULT_COMPRESSIONLEVEL; private boolean useNativeTransport = DEFAULT_USE_NATIVE_TRANSPORT; private String fallbackProtocol = ApplicationProtocolNames.HTTP_1_1; + private AccessLogger accessLogger; /** * Default empty constructor. @@ -141,6 +142,22 @@ public NettyHttpServerConfiguration( this.pipelineCustomizers = pipelineCustomizers; } + /** + * Returns the AccessLogger configuration. + * @return The AccessLogger configuration. + */ + public AccessLogger getAccessLogger() { + return accessLogger; + } + + /** + * Sets the AccessLogger configuration. + * @param accessLogger The configuration . + */ + public void setAccessLogger(AccessLogger accessLogger) { + this.accessLogger = accessLogger; + } + /** * @return The pipeline customizers */ @@ -402,6 +419,65 @@ public void setCompressionLevel(@ReadableBytes int compressionLevel) { this.compressionLevel = compressionLevel; } + /** + * Access logger configuration. + */ + @ConfigurationProperties("access-logger") + public static class AccessLogger { + private boolean enabled; + private String loggerName; + private String logFormat; + + /** + * Returns whether the access logger is enabled. + * @return Whether the access logger is enabled. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Enables or Disables the access logger. + * @param enabled The flag. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * The logger name to use. Access logs will be logged at info level. + * @return The logger name. + */ + public String getLoggerName() { + return loggerName; + } + + /** + * Sets the logger name to use. If not specified 'HTTP_ACCESS_LOGGER' will be used. + * @param loggerName A logger name, + */ + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + /** + * Returns the log format to use. + * @return The log format. + */ + public String getLogFormat() { + return logFormat; + } + + /** + * Sets the log format to use. When not specified, the Common Log Format (CLF) will be used. + * @param logFormat The log format. + */ + public void setLogFormat(String logFormat) { + this.logFormat = logFormat; + } + + } + /** * Configuration for Netty worker. */ diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/HttpAccessLogHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/HttpAccessLogHandler.java new file mode 100644 index 00000000000..49ebf1b9a40 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/HttpAccessLogHandler.java @@ -0,0 +1,157 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.micronaut.http.server.netty.handler.accesslog.element.AccessLogFormatParser; +import io.micronaut.http.server.netty.handler.accesslog.element.AccessLog; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; + +/** + * Logging handler for HTTP access logs. + * Access logs will be logged at info level. + * + * @author croudet + * @since 2.0 + */ +@Sharable +public class HttpAccessLogHandler extends ChannelDuplexHandler { + /** + * The default logger name. + */ + public static final String HTTP_ACCESS_LOGGER = "HTTP_ACCESS_LOGGER"; + + private static final AttributeKey ACCESS_LOGGER = AttributeKey.valueOf("ACCESS_LOGGER"); + private static final String H2_PROTOCOL_NAME = "HTTP/2.0"; + + private final Logger logger; + private final AccessLogFormatParser accessLogFormatParser; + + /** + * Creates a HttpAccessLogHandler. + * + * @param loggerName A logger name. + * @param spec The log format specification. + */ + public HttpAccessLogHandler(String loggerName, String spec) { + this(loggerName == null || loggerName.isEmpty() ? null : LoggerFactory.getLogger(loggerName), spec); + } + + /** + * Creates a HttpAccessLogHandler. + * + * @param logger A logger. Will log at info level. + * @param spec The log format specification. + */ + public HttpAccessLogHandler(Logger logger, String spec) { + super(); + this.logger = logger == null ? LoggerFactory.getLogger(HTTP_ACCESS_LOGGER) : logger; + this.accessLogFormatParser = new AccessLogFormatParser(spec); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Http2Exception { + if (logger.isInfoEnabled() && msg instanceof HttpRequest) { + final SocketChannel channel = (SocketChannel) ctx.channel(); + final HttpRequest request = (HttpRequest) msg; + final HttpHeaders headers = request.headers(); + // Trying to detect http/2 + String protocol; + if (headers.contains(ExtensionHeaderNames.STREAM_ID.text()) || headers.contains(ExtensionHeaderNames.SCHEME.text())) { + protocol = H2_PROTOCOL_NAME; + } else { + protocol = request.protocolVersion().text(); + } + accessLog(channel).onRequestHeaders(channel, request.method().name(), request.headers(), request.uri(), protocol); + } + ctx.fireChannelRead(msg); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (logger.isInfoEnabled()) { + processWriteEvent(ctx, msg, promise); + } else { + super.write(ctx, msg, promise); + } + } + + private AccessLog accessLog(SocketChannel channel) { + final Attribute attr = channel.attr(ACCESS_LOGGER); + AccessLog accessLog = attr.get(); + if (accessLog == null) { + accessLog = accessLogFormatParser.newAccessLogger(); + attr.set(accessLog); + } else { + accessLog.reset(); + } + return accessLog; + } + + private void log(ChannelHandlerContext ctx, Object msg, ChannelPromise promise, AccessLog accessLog) { + ctx.write(msg, promise.unvoid()).addListener(future -> { + if (future.isSuccess()) { + accessLog.log(logger); + } + }); + } + + private static boolean processHttpResponse(HttpResponse response, AccessLog accessLogger, ChannelHandlerContext ctx, ChannelPromise promise) { + final HttpResponseStatus status = response.status(); + if (status.equals(HttpResponseStatus.CONTINUE)) { + ctx.write(response, promise); + return true; + } + accessLogger.onResponseHeaders(ctx, response.headers(), status.codeAsText().toString()); + return false; + } + + private void processWriteEvent(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + final AccessLog accessLogger = ctx.channel().attr(ACCESS_LOGGER).get(); + if (accessLogger != null) { + if (msg instanceof HttpResponse && processHttpResponse((HttpResponse) msg, accessLogger, ctx, promise)) { + return; + } + if (msg instanceof LastHttpContent) { + accessLogger.onLastResponseWrite(((LastHttpContent) msg).content().readableBytes()); + log(ctx, msg, promise, accessLogger); + return; + } else if (msg instanceof ByteBufHolder) { + accessLogger.onResponseWrite(((ByteBufHolder) msg).content().readableBytes()); + } else if (msg instanceof ByteBuf) { + accessLogger.onResponseWrite(((ByteBuf) msg).readableBytes()); + } + } + super.write(ctx, msg, promise); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AbstractHttpMessageLogElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AbstractHttpMessageLogElement.java new file mode 100644 index 00000000000..ce6319a44b4 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AbstractHttpMessageLogElement.java @@ -0,0 +1,94 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * LogElement for ON_REQUEST_HEADERS and ON_RESPONSE_HEADERS events. + * + * @author croudet + * @since 2.0 + */ +abstract class AbstractHttpMessageLogElement implements LogElement { + protected Set events; + + /** + * Process the specified headers. + * @param headers Http headers. + * @return The value. + */ + protected abstract String value(HttpHeaders headers); + + private static String wrapValue(String value) { + // Does the value contain a " ? If so must encode it + if (value == null || ConstantElement.UNKNOWN_VALUE.equals(value) || value.isEmpty()) { + return ConstantElement.UNKNOWN_VALUE; + } + + /* Wrap all quotes in double quotes. */ + StringBuilder buffer = new StringBuilder(value.length() + 2); + buffer.append('\''); + int i = 0; + while (i < value.length()) { + int j = value.indexOf('\'', i); + if (j == -1) { + buffer.append(value.substring(i)); + i = value.length(); + } else { + buffer.append(value.substring(i, j + 1)); + buffer.append('"'); + i = j + 2; + } + } + + buffer.append('\''); + return buffer.toString(); + } + + @Override + public Set events() { + return events; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + if (events.contains(Event.ON_REQUEST_HEADERS)) { + return wrapValue(value(headers)); + } else { + return ConstantElement.UNKNOWN_VALUE; + } + } + + @Override + public String onResponseHeaders(ChannelHandlerContext ctx, HttpHeaders headers, String status) { + if (events.contains(Event.ON_RESPONSE_HEADERS)) { + return wrapValue(value(headers)); + } else { + return ConstantElement.UNKNOWN_VALUE; + } + } + + @Override + public LogElement copy() { + return this; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLog.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLog.java new file mode 100644 index 00000000000..f5ea3e1bdd3 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLog.java @@ -0,0 +1,134 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.List; + +import org.slf4j.Logger; + +import io.micronaut.http.server.netty.handler.accesslog.element.AccessLogFormatParser.IndexedLogElement; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * An Access log instance. + * + * @author croudet + * @since 2.0 + */ +public class AccessLog { + private final List onRequestHeadersElements; + private final List onResponseHeadersElements; + private final List onResponseWriteElements; + private final List onLastResponseWriteElements; + private final String[] elements; + + /** + * Creates an AccessLog. + * + * @param onRequestHeadersElements The LogElements that depends on the ON_REQUEST_HEADERS event. + * @param onResponseHeadersElements The LogElements that depends on the ON_RESPONSE_HEADERS event. + * @param onResponseWriteElements The LogElements that depends on the ON_WRITE_RESPONSE event. + * @param onLastResponseWriteElements The LogElements that depends on the ON_LAST_WRITE_RESPONSE event. + * @param elements The array of values. + */ + AccessLog(List onRequestHeadersElements, List onResponseHeadersElements, List onResponseWriteElements, List onLastResponseWriteElements, String[] elements) { + this.onRequestHeadersElements = onRequestHeadersElements; + this.onResponseHeadersElements = onResponseHeadersElements; + this.onResponseWriteElements = onResponseWriteElements; + this.onLastResponseWriteElements = onLastResponseWriteElements; + this.elements = elements; + } + + /** + * Resets the current values. + */ + public void reset() { + onRequestHeadersElements.forEach(this::resetIndexedLogElement); + onResponseHeadersElements.forEach(this::resetIndexedLogElement); + onResponseWriteElements.forEach(this::resetIndexedLogElement); + onLastResponseWriteElements.forEach(this::resetIndexedLogElement); + } + + /** + * Triggers LogElements for the ON_REQUEST_HEADERS event. + * + * @param channel The socket channel. + * @param method The http method. + * @param headers The request headers. + * @param uri The uri. + * @param protocol The protocol. + */ + public void onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + for (IndexedLogElement element: onRequestHeadersElements) { + elements[element.index] = element.onRequestHeaders(channel, method, headers, uri, protocol); + } + } + + /** + * Triggers LogElements for the ON_RESPONSE_HEADERS event. + * + * @param ctx The ChannelHandlerContext. + * @param headers The response headers. + * @param status The response status. + */ + public void onResponseHeaders(ChannelHandlerContext ctx, HttpHeaders headers, String status) { + for (IndexedLogElement element: onResponseHeadersElements) { + elements[element.index] = element.onResponseHeaders(ctx, headers, status); + } + } + + /** + * Triggers LogElements for the ON_RESPONSE_WRITE event. + * @param bytesSent The number of bytes sent. + */ + public void onResponseWrite(int bytesSent) { + for (IndexedLogElement element: onResponseWriteElements) { + element.onResponseWrite(bytesSent); + } + } + + /** + * Triggers LogElements for the ON_LAST_RESPONSE_WRITE event. + * @param bytesSent The number of bytes sent. + */ + public void onLastResponseWrite(int bytesSent) { + for (IndexedLogElement element: onLastResponseWriteElements) { + elements[element.index] = element.onLastResponseWrite(bytesSent); + } + } + + /** + * Logs at info level the accumulated values. + * + * @param accessLogger A logger. + */ + public void log(Logger accessLogger) { + if (accessLogger.isInfoEnabled()) { + final StringBuilder b = new StringBuilder(elements.length * 5); + for (int i = 0; i < elements.length; ++i) { + b.append(elements[i] == null ? ConstantElement.UNKNOWN_VALUE : elements[i]); + } + accessLogger.info(b.toString()); + } + } + + private void resetIndexedLogElement(IndexedLogElement elt) { + elements[elt.index] = null; + elt.reset(); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParser.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParser.java new file mode 100644 index 00000000000..0b187874c8f --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParser.java @@ -0,0 +1,360 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.micronaut.core.io.service.ServiceDefinition; +import io.micronaut.core.io.service.SoftServiceLoader; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.SocketChannel; + +/** + * The access log format parser. + * + * The syntax is based on Apache httpd log format. + * Here are the supported directives: + *
    + *
  • %a - Remote IP address
  • + *
  • %A - Local IP address
  • + *
  • %b - Bytes sent, excluding HTTP headers, or '-' if no bytes were sent
  • + *
  • %B - Bytes sent, excluding HTTP headers
  • + *
  • %h - Remote host name
  • + *
  • %H - Request protocol
  • + *
  • %{
    }i - Request header. If the argument is omitted (%i) all headers will be printed
  • + *
  • %{
    }o - Response header. If the argument is omitted (%o) all headers will be printed
  • + *
  • %{}C - Request cookie (COOKIE). If the argument is omitted (%C) all cookies will be printed
  • + *
  • %{}c - Response cookie (SET_COOKIE). If the argument is omitted (%c) all cookies will be printed
  • + *
  • %l - Remote logical username from identd (always returns '-')
  • + *
  • %m - Request method
  • + *
  • %p - Local port
  • + *
  • %q - Query string (excluding the '?' character)
  • + *
  • %r - First line of the request
  • + *
  • %s - HTTP status code of the response
  • + *
  • %{}t - Date and time. If the argument is ommitted the Common Log Format format is used ("'['dd/MMM/yyyy:HH:mm:ss Z']'"). + * If the format starts with begin: (default) the time is taken at the beginning of the request processing. If it starts with end: it is the time when the log entry gets written, close to the end of the request processing. + * The format should follow the DateTimeFormatter syntax.
  • + *
  • %u - Remote user that was authenticated. Not implemented. Prints '-'.
  • + *
  • %U - Requested URI
  • + *
  • %v - Local server name
  • + *
  • %D - Time taken to process the request, in millis
  • + *
  • %T - Time taken to process the request, in seconds
  • + *
+ *

In addition, the following aliases for commonly utilized patterns:

+ *
    + *
  • common - %h %l %u %t "%r" %s %b Common Log Format (CLF)
  • + *
  • combined - + * %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" Combined Log Format
  • + *
+ * + * @author croudet + * @since 2.0 + */ +public class AccessLogFormatParser { + /** + * The combined log format. + */ + public static final String COMBINED_LOG_FORMAT = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\""; + /** + * The common log format. + */ + public static final String COMMON_LOG_FORMAT = "%h %l %u %t \"%r\" %s %b"; + + private static final List LOG_ELEMENT_BUILDERS; + + private static final Logger LOGGER = LoggerFactory.getLogger(AccessLogFormatParser.class); + + private final List onRequestElements = new ArrayList<>(); + private final List onResponseHeadersElements = new ArrayList<>(); + private final List onResponseWriteElements = new ArrayList<>(); + private final List onLastResponseWriteElements = new ArrayList<>(); + private final List constantElements = new ArrayList<>(); + private String[] elements; + + static { + SoftServiceLoader builders = SoftServiceLoader.load(LogElementBuilder.class, LogElementBuilder.class.getClassLoader()); + LOG_ELEMENT_BUILDERS = new ArrayList<>(); + for (ServiceDefinition definition : builders) { + if (definition.isPresent()) { + LOG_ELEMENT_BUILDERS.add(definition.load()); + } + } + trimToSize(LOG_ELEMENT_BUILDERS); + } + + /** + * Creates an AccessLogFormatParser. + * + * @param spec The log format. When null the Common Log Format is used. + */ + public AccessLogFormatParser(String spec) { + parse(spec); + } + + /** + * Returns a new AccessLogger for the specified log format. + * + * @return A AccessLogger. + */ + public AccessLog newAccessLogger() { + String[] newElements = new String[elements.length]; + System.arraycopy(elements, 0, newElements, 0, elements.length); + Map map = new IdentityHashMap<>(); + return new AccessLog(copy(map, onRequestElements), copy(map, onResponseHeadersElements), copy(map, onResponseWriteElements), copy(map, onLastResponseWriteElements), newElements); + } + + @Override + public String toString() { + SortedSet elts = new TreeSet<>(); + elts.addAll(constantElements); + elts.addAll(onLastResponseWriteElements); + elts.addAll(onRequestElements); + elts.addAll(onResponseHeadersElements); + elts.addAll(onResponseWriteElements); + return elts.stream().map(IndexedLogElement::toString).collect(Collectors.joining()); + } + + private static List copy(Map map, List l) { + return l.stream().map(elt -> map.computeIfAbsent(elt, IndexedLogElement::copyIndexedLogElement)).collect(Collectors.toList()); + } + + private void parse(String spec) { + if (spec == null || spec.isEmpty() || "common".equals(spec)) { + spec = COMMON_LOG_FORMAT; + } else if ("combined".equals(spec)) { + spec = COMBINED_LOG_FORMAT; + } + List logElements = tokenize(spec); + elements = new String[logElements.size()]; + for (int i = 0; i < elements.length; ++i) { + LogElement element = logElements.get(i); + IndexedLogElement indexedLogElement = new IndexedLogElement(element, i); + if (element.events().isEmpty()) { + // constants + constantElements.add(indexedLogElement); + // pre-fill log values with constant + elements[i] = element.onRequestHeaders(null, null, null, null, null); + continue; + } + if (element.events().contains(LogElement.Event.ON_LAST_RESPONSE_WRITE)) { + onLastResponseWriteElements.add(indexedLogElement); + } + if (element.events().contains(LogElement.Event.ON_REQUEST_HEADERS)) { + onRequestElements.add(indexedLogElement); + } + if (element.events().contains(LogElement.Event.ON_RESPONSE_HEADERS)) { + onResponseHeadersElements.add(indexedLogElement); + } + if (element.events().contains(LogElement.Event.ON_RESPONSE_WRITE)) { + onResponseWriteElements.add(indexedLogElement); + } + } + trimToSize(onLastResponseWriteElements); + trimToSize(onRequestElements); + trimToSize(onResponseHeadersElements); + trimToSize(onResponseWriteElements); + trimToSize(constantElements); + } + + private static void trimToSize(List l) { + ((ArrayList) l).trimToSize(); + } + + private List tokenize(String spec) { + List logElements = new ArrayList<>(); + spec = spec.trim(); + int state = 0; + StringBuilder token = new StringBuilder(40); + for (int i = 0; i < spec.length(); ++i) { + char c = spec.charAt(i); + state = nextState(logElements, state, token, c); + } + if (state != 0 || logElements.isEmpty()) { + LOGGER.warn("Invalid access log format: {}", spec); + throw new IllegalArgumentException("Invalid access log format: " + spec); + } + checkConstantElement(logElements, token); + return logElements; + } + + private int nextState(List logElements, int state, StringBuilder token, char c) { + switch (state) { + case 0: + // --> spacer + if (c == '%') { + state = 1; + } else { + token.append(c); + } + break; + case 1: + // --> % + if (c == '{') { + checkConstantElement(logElements, token); + state = 2; + } else if (c == '%') { + // escape literal + token.append(c); + state = 0; + } else { + checkConstantElement(logElements, token); + logElements.add(fromToken(Character.toString(c), null)); + state = 0; + } + break; + case 2: + // --> %{ + if (c == '}') { + state = 3; + } else { + token.append(c); + } + break; + case 3: + // --> %{<>} + String param = token.toString(); + logElements.add(fromToken(Character.toString(c), param)); + token.setLength(0); + state = 0; + break; + default: + // ignore + break; + } + return state; + } + + private void checkConstantElement(List logElements, StringBuilder token) { + if (token.length() != 0) { + logElements.add(new ConstantElement(token.toString())); + token.setLength(0); + } + } + + private LogElement fromToken(String pattern, String param) { + for (LogElementBuilder builder: LOG_ELEMENT_BUILDERS) { + LogElement logElement = builder.build(pattern, param); + if (logElement != null) { + return logElement; + } + } + LOGGER.warn("Unknown access log marker: %{}", pattern); + return ConstantElement.UNKNOWN; + } + + /** + * A log element with an index that specifies its position in the log format. + * @author croudet + */ + static class IndexedLogElement implements LogElement, Comparable { + final int index; + private final LogElement delegate; + + /** + * Creates an IndexedLogElement. + * @param delegate A LogElement. + * @param index The index. + */ + IndexedLogElement(LogElement delegate, int index) { + this.delegate = delegate; + this.index = index; + } + + @Override + public Set events() { + return delegate.events(); + } + + @Override + public void reset() { + delegate.reset(); + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, io.netty.handler.codec.http.HttpHeaders headers, String uri, String protocol) { + return delegate.onRequestHeaders(channel, method, headers, uri, protocol); + } + + @Override + public String onResponseHeaders(ChannelHandlerContext ctx, io.netty.handler.codec.http.HttpHeaders headers, String status) { + return delegate.onResponseHeaders(ctx, headers, status); + } + + @Override + public void onResponseWrite(int contentSize) { + delegate.onResponseWrite(contentSize); + } + + @Override + public String onLastResponseWrite(int contentSize) { + return delegate.onLastResponseWrite(contentSize); + } + + @Override + public LogElement copy() { + return new IndexedLogElement(delegate.copy(), index); + } + + /** + * Returns a copy of this element. + * @return A copy of this element. + */ + public IndexedLogElement copyIndexedLogElement() { + return new IndexedLogElement(delegate.copy(), index); + } + + @Override + public int compareTo(IndexedLogElement o) { + return Long.compare(index, o.index); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public int hashCode() { + return index; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + IndexedLogElement other = (IndexedLogElement) obj; + return index == other.index; + } + + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/BytesSentElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/BytesSentElement.java new file mode 100644 index 00000000000..26d313a1229 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/BytesSentElement.java @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +/** + * BytesSentElement LogElement. The bytes sent. + * + * @author croudet + * @since 2.0 + */ +final class BytesSentElement implements LogElement { + /** + * The bytes sent marker (set dask when 0.) + */ + public static final String BYTES_SENT_DASH = "b"; + /** + * The bytes sent marker. + */ + public static final String BYTES_SENT = "B"; + + private static final Set EVENTS = Collections.unmodifiableSet(EnumSet.of(Event.ON_RESPONSE_WRITE, Event.ON_LAST_RESPONSE_WRITE)); + + private final boolean dashIfZero; + private long bytesSent; + + /** + * Creates a BytesSentElement. + * @param dashIfZero When true, use '-' when bytes sent is 0. + */ + BytesSentElement(final boolean dashIfZero) { + this.dashIfZero = dashIfZero; + } + + @Override + public void onResponseWrite(int contentSize) { + bytesSent += contentSize; + } + + @Override + public String onLastResponseWrite(int contentSize) { + bytesSent += contentSize; + return dashIfZero && bytesSent == 0L ? ConstantElement.UNKNOWN_VALUE : Long.toString(bytesSent); + } + + @Override + public Set events() { + return EVENTS; + } + + @Override + public LogElement copy() { + return new BytesSentElement(dashIfZero); + } + + @Override + public void reset() { + bytesSent = 0L; + } + + @Override + public String toString() { + return '%' + (dashIfZero ? BYTES_SENT_DASH : BYTES_SENT); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/BytesSentElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/BytesSentElementBuilder.java new file mode 100644 index 00000000000..cf20df9e884 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/BytesSentElementBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for BytesSentElement. + * + * @author croudet + * @since 2.0 + */ +public final class BytesSentElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (BytesSentElement.BYTES_SENT_DASH.equals(token)) { + return new BytesSentElement(true); + } else if (BytesSentElement.BYTES_SENT.equals(token)) { + return new BytesSentElement(false); + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ConstantElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ConstantElement.java new file mode 100644 index 00000000000..f49dd951685 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ConstantElement.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * ConstantElement LogElement. Represents a fixed value. + * + * @author croudet + * @since 2.0 + */ +public final class ConstantElement implements LogElement { + + /** + * The unknown value: '-' + */ + public static final String UNKNOWN_VALUE = "-"; + + /** + * The unknown LogElement. + */ + public static final ConstantElement UNKNOWN = new ConstantElement(UNKNOWN_VALUE); + + private static final Set EVENTS = Collections.unmodifiableSet(EnumSet.noneOf(Event.class)); + + private final String value; + + /** + * Creates a constant LogElement. + * + * @param value The constant value. + */ + ConstantElement(final String value) { + this.value = value; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return value; + } + + @Override + public Set events() { + return EVENTS; + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String toString() { + return value.replace("%", "%%"); + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookieElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookieElement.java new file mode 100644 index 00000000000..42614cdad61 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookieElement.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; + +/** + * CookieElement LogElement. A cookie. + * + * @author croudet + * @since 2.0 + */ +final class CookieElement extends AbstractHttpMessageLogElement { + /** + * The request cookie marker. + */ + public static final String REQUEST_COOKIE = "C"; + /** + * The response cookier marker. + */ + public static final String RESPONSE_COOKIE = "c"; + + private final String headerName; + private final String cookieName; + + /** + * Creates a CookieLogElement. + * + * @param forRequest true for request cookie, false for response cookie. + * @param cookieName The cookie name. + */ + CookieElement(boolean forRequest, final String cookieName) { + this.cookieName = cookieName; + this.headerName = forRequest ? HttpHeaderNames.COOKIE.toString() : HttpHeaderNames.SET_COOKIE.toString(); + this.events = forRequest ? Event.REQUEST_HEADERS_EVENTS : Event.RESPONSE_HEADERS_EVENTS; + } + + @Override + protected String value(HttpHeaders headers) { + String header = headers.get(headerName); + if (header != null) { + for (Cookie cookie: ServerCookieDecoder.STRICT.decodeAll(header)) { + if (cookieName.equals(cookie.name())) { + return cookie.value(); + } + } + } + return ConstantElement.UNKNOWN_VALUE; + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String toString() { + return "%{" + cookieName + '}' + (HttpHeaderNames.COOKIE.toString().equals(this.headerName) ? REQUEST_COOKIE : RESPONSE_COOKIE); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookieElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookieElementBuilder.java new file mode 100644 index 00000000000..d2fbd6bcaa2 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookieElementBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for CookieElement and CookiesElement. + * + * @author croudet + * @since 2.0 + */ +public final class CookieElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (CookieElement.REQUEST_COOKIE.equals(token)) { + return param == null ? CookiesElement.forRequest() : new CookieElement(true, param); + } else if (CookieElement.RESPONSE_COOKIE.equals(token)) { + return param == null ? CookiesElement.forResponse() : new CookieElement(false, param); + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookiesElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookiesElement.java new file mode 100644 index 00000000000..f601340b955 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/CookiesElement.java @@ -0,0 +1,100 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.List; +import java.util.StringJoiner; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; + +/** + * CookiesElement LogElement. All cookies. + * + * @author croudet + * @since 2.0 + */ +final class CookiesElement extends AbstractHttpMessageLogElement { + /** + * The request cookie marker. + */ + public static final String REQUEST_COOKIES = CookieElement.REQUEST_COOKIE; + /** + * The response cookie marker. + */ + public static final String RESPONSE_COOKIES = CookieElement.RESPONSE_COOKIE; + + private static final CookiesElement REQUEST_COOKIES_ELEMENT = new CookiesElement(HttpHeaderNames.COOKIE.toString()); + private static final CookiesElement RESPONSE_COOKIES_ELEMENT = new CookiesElement(HttpHeaderNames.SET_COOKIE.toString()); + + private final String headerName; + + private CookiesElement(String headerName) { + if (HttpHeaderNames.COOKIE.toString().equals(headerName) || HttpHeaderNames.SET_COOKIE.toString().equals(headerName)) { + this.headerName = headerName; + } else { + this.headerName = HttpHeaderNames.COOKIE.toString(); + } + this.events = HttpHeaderNames.COOKIE.toString().equals(this.headerName) ? Event.REQUEST_HEADERS_EVENTS : Event.RESPONSE_HEADERS_EVENTS; + } + + /** + * CookiesElement for request. + * + * @return CookiesElement for request. + */ + public static CookiesElement forRequest() { + return REQUEST_COOKIES_ELEMENT; + } + + /** + * CookiesElement for response. + * + * @return CookiesElement for response. + */ + public static CookiesElement forResponse() { + return RESPONSE_COOKIES_ELEMENT; + } + + @Override + protected String value(HttpHeaders headers) { + final String header = headers.get(headerName); + if (header != null) { + final List cookies = ServerCookieDecoder.STRICT.decodeAll(header); + if (cookies.isEmpty()) { + return ConstantElement.UNKNOWN_VALUE; + } + if (cookies.size() == 1) { + final Cookie cookie = cookies.iterator().next(); + return cookie.name() + ':' + cookie.value(); + } + final StringJoiner joiner = new StringJoiner(",", "[", "]"); + for (Cookie cookie: cookies) { + joiner.add(cookie.name() + ':' + cookie.value()); + } + return joiner.toString(); + } + return ConstantElement.UNKNOWN_VALUE; + } + + @Override + public String toString() { + return '%' + (HttpHeaderNames.COOKIE.toString().equals(this.headerName) ? CookieElement.REQUEST_COOKIE : CookieElement.RESPONSE_COOKIE); + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/DateTimeElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/DateTimeElement.java new file mode 100644 index 00000000000..bc565f99414 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/DateTimeElement.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Locale; +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * DateTimeElement LogElement. + * + * @author croudet + * @since 2.0 + */ +final class DateTimeElement implements LogElement { + + /** + * The date/time marker. + */ + public static final String DATE_TIME = "t"; + + private static final String COMMON_LOG_PATTERN = "'['dd/MMM/yyyy:HH:mm:ss Z']'"; + + private static final Set LAST_RESPONSE_EVENTS = Collections.unmodifiableSet(EnumSet.of(Event.ON_LAST_RESPONSE_WRITE)); + + private final DateTimeFormatter formatter; + private final Set events; + private final String dateFormat; + + /** + * Create a DateTimeElement. + * + * @param dateFormat The date time format. DateTimeFormtter is used. The format can start with "begin:" or "end:" + * If the format starts with begin: (default) the time is taken at the beginning of the request processing. + * If it starts with end: it is the time when the log entry gets written, close to the end of the request processing. + */ + DateTimeElement(final String dateFormat) { + boolean fromStart; + String format; + if (dateFormat == null) { + format = COMMON_LOG_PATTERN; + fromStart = true; + } else { + fromStart = ! dateFormat.startsWith("end:"); + if (dateFormat.startsWith("begin:")) { + format = dateFormat.substring("begin:".length()); + fromStart = true; + } else if (dateFormat.startsWith("end:")) { + format = dateFormat.substring("end:".length()); + fromStart = false; + } else { + format = dateFormat; + } + } + this.dateFormat = dateFormat; + formatter = DateTimeFormatter.ofPattern(format, Locale.US); + events = fromStart ? Event.REQUEST_HEADERS_EVENTS : LAST_RESPONSE_EVENTS; + } + + @Override + public Set events() { + return events; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + if (events.contains(Event.ON_REQUEST_HEADERS)) { + return ZonedDateTime.now().format(formatter); + } else { + return ConstantElement.UNKNOWN_VALUE; + } + } + + @Override + public String onLastResponseWrite(int contentSize) { + if (events.contains(Event.ON_LAST_RESPONSE_WRITE)) { + return ZonedDateTime.now().format(formatter); + } else { + return ConstantElement.UNKNOWN_VALUE; + } + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String toString() { + return dateFormat == null ? '%' + DATE_TIME : "%{" + dateFormat + '}' + DATE_TIME; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/DateTimeElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/DateTimeElementBuilder.java new file mode 100644 index 00000000000..f60b87b5470 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/DateTimeElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for DateTimeElement. + * + * @author croudet + * @since 2.0 + */ +public final class DateTimeElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (DateTimeElement.DATE_TIME.equals(token)) { + return new DateTimeElement(param); + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ElapseTimeElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ElapseTimeElement.java new file mode 100644 index 00000000000..647874e8d9f --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ElapseTimeElement.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * ElapseTimeElement LogElement. Time spent to complete the request. + * + * @author croudet + * @since 2.0 + */ +final class ElapseTimeElement implements LogElement { + /** + * The elapse time marker (seconds.) + */ + public static final String ELAPSE_TIME_SECONDS = "T"; + /** + * The elapse time marker (milliseconds.) + */ + public static final String ELAPSE_TIME_MILLIS = "D"; + + private static final Set EVENTS = Collections.unmodifiableSet(EnumSet.of(Event.ON_REQUEST_HEADERS, Event.ON_LAST_RESPONSE_WRITE)); + + private final boolean inSeconds; + private long start; + + /** + * Create an ElapseTimeElement. + * + * @param inSeconds When true time is will be printed in seconds otherwise in millisecond. + */ + ElapseTimeElement(final boolean inSeconds) { + this.inSeconds = inSeconds; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + start = System.nanoTime(); + return null; + } + + @Override + public String onLastResponseWrite(int contentSize) { + final long elapseTime = inSeconds ? TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) : TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + return Long.toString(elapseTime); + } + + @Override + public Set events() { + return EVENTS; + } + + @Override + public LogElement copy() { + return new ElapseTimeElement(inSeconds); + } + + @Override + public void reset() { + start = 0L; + } + + @Override + public String toString() { + return '%' + (inSeconds ? ELAPSE_TIME_SECONDS : ELAPSE_TIME_MILLIS); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ElapseTimeElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ElapseTimeElementBuilder.java new file mode 100644 index 00000000000..26da67f6cca --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ElapseTimeElementBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for ElapseTimeElement. + * + * @author croudet + * @since 2.0 + */ +public final class ElapseTimeElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (ElapseTimeElement.ELAPSE_TIME_MILLIS.equals(token)) { + return new ElapseTimeElement(false); + } else if (ElapseTimeElement.ELAPSE_TIME_SECONDS.equals(token)) { + return new ElapseTimeElement(true); + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeaderElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeaderElement.java new file mode 100644 index 00000000000..15339e4364a --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeaderElement.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.List; +import java.util.StringJoiner; + +import io.netty.handler.codec.http.HttpHeaders; + +/** + * HeaderElement LogElement. A http header. + * + * @author croudet + * @since 2.0 + */ +final class HeaderElement extends AbstractHttpMessageLogElement { + /** + * The request header marker. + */ + public static final String REQUEST_HEADER = "i"; + /** + * The response header marker. + */ + public static final String RESPONSE_HEADER = "o"; + + private final String header; + + /** + * Creates a HeaderElement. + * + * @param onRequest When true, retrieves header from request, otherwise from response. + * @param header The header name. + */ + HeaderElement(boolean onRequest, final String header) { + this.header = header; + this.events = onRequest ? Event.REQUEST_HEADERS_EVENTS : Event.RESPONSE_HEADERS_EVENTS; + } + + @Override + protected String value(HttpHeaders headers) { + final List values = headers.getAllAsString(header); + if (values.isEmpty()) { + return ConstantElement.UNKNOWN_VALUE; + } + if (values.size() == 1) { + return values.iterator().next(); + } + final StringJoiner joiner = new StringJoiner(",", "[", "]"); + for (String v: values) { + joiner.add(v); + } + return joiner.toString(); + } + + @Override + public String toString() { + return "%{" + header + '}' + (events.contains(Event.ON_REQUEST_HEADERS) ? REQUEST_HEADER : RESPONSE_HEADER); + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeaderElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeaderElementBuilder.java new file mode 100644 index 00000000000..4277cb3f212 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeaderElementBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for Headerlement and HeadersElement. + * + * @author croudet + * @since 2.0 + */ +public final class HeaderElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (HeaderElement.REQUEST_HEADER.equals(token)) { + return param == null ? HeadersElement.forRequest() : new HeaderElement(true, param); + } else if (HeaderElement.RESPONSE_HEADER.equals(token)) { + return param == null ? HeadersElement.forResponse() : new HeaderElement(false, param); + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeadersElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeadersElement.java new file mode 100644 index 00000000000..214fd8462e0 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/HeadersElement.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Map.Entry; +import java.util.StringJoiner; + +import io.netty.handler.codec.http.HttpHeaders; + +/** + * HeadersElement LogElement. All http headers. + * + * @author croudet + * @since 2.0 + */ +final class HeadersElement extends AbstractHttpMessageLogElement { + + private static final HeadersElement REQUEST_HEADERS_ELEMENT = new HeadersElement(true); + private static final HeadersElement RESPONSE_HEADERS_ELEMENT = new HeadersElement(false); + + private HeadersElement(boolean onRequest) { + this.events = onRequest ? Event.REQUEST_HEADERS_EVENTS : Event.RESPONSE_HEADERS_EVENTS; + } + + /** + * Returns the HeadersElement for request. + * @return The HeadersElement for request. + */ + public static HeadersElement forRequest() { + return REQUEST_HEADERS_ELEMENT; + } + + /** + * Returns the HeadersElement for response. + * @return The HeadersElement for response. + */ + public static HeadersElement forResponse() { + return RESPONSE_HEADERS_ELEMENT; + } + + @Override + protected String value(HttpHeaders headers) { + if (headers.isEmpty()) { + return ConstantElement.UNKNOWN_VALUE; + } + if (headers.size() == 1) { + final Entry header = headers.iteratorCharSequence().next(); + return header.getKey() + ":" + header.getValue(); + } + final StringJoiner joiner = new StringJoiner(",", "[", "]"); + headers.forEach(header -> joiner.add(header.getKey() + ':' + header.getValue())); + return joiner.toString(); + } + + @Override + public String toString() { + return events.contains(Event.ON_REQUEST_HEADERS) ? HeaderElement.REQUEST_HEADER : HeaderElement.RESPONSE_HEADER; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalHostElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalHostElement.java new file mode 100644 index 00000000000..67a1297a509 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalHostElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * LocalHostElement LogElement. The local host. + * + * @author croudet + * @since 2.0 + */ +final class LocalHostElement implements LogElement { + /** + * The local host marker. + */ + public static final String LOCAL_HOST = "v"; + + /** + * The LocalHostElement instance. + */ + static final LocalHostElement INSTANCE = new LocalHostElement(); + + private LocalHostElement() { + + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return channel.localAddress().getAddress().getHostName(); + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String toString() { + return '%' + LOCAL_HOST; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalHostElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalHostElementBuilder.java new file mode 100644 index 00000000000..1565beb9c60 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalHostElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for LocalHostElement. + * + * @author croudet + * @since 2.0 + */ +public final class LocalHostElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (LocalHostElement.LOCAL_HOST.equals(token)) { + return LocalHostElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalIpElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalIpElement.java new file mode 100644 index 00000000000..695e5d7dcd0 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalIpElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * LocalIpElement LogElement. The local IP address. + * + * @author croudet + * @since 2.0 + */ +final class LocalIpElement implements LogElement { + /** + * The local ip marker. + */ + public static final String LOCAL_IP = "A"; + + /** + * The LocalIpElement instance. + */ + static final LocalIpElement INSTANCE = new LocalIpElement(); + + private LocalIpElement() { + + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return channel.localAddress().getAddress().getHostAddress(); + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String toString() { + return '%' + LOCAL_IP; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalIpElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalIpElementBuilder.java new file mode 100644 index 00000000000..1d63c292acb --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalIpElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for LocalIpElement. + * + * @author croudet + * @since 2.0 + */ +public final class LocalIpElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (LocalIpElement.LOCAL_IP.equals(token)) { + return LocalIpElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalPortElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalPortElement.java new file mode 100644 index 00000000000..517b7d025ab --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalPortElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * LocalPortElement LogElement. The local port. + * + * @author croudet + * @since 2.0 + */ +final class LocalPortElement implements LogElement { + /** + * The local port marker. + */ + public static final String LOCAL_PORT = "p"; + + /** + * The LocalPortElement instance. + */ + static final LocalPortElement INSTANCE = new LocalPortElement(); + + private LocalPortElement() { + + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return Integer.toString(channel.localAddress().getPort()); + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String toString() { + return '%' + LOCAL_PORT; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalPortElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalPortElementBuilder.java new file mode 100644 index 00000000000..3dec0f3460e --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LocalPortElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for LocalPortElement. + * + * @author croudet + * @since 2.0 + */ +public final class LocalPortElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (LocalPortElement.LOCAL_PORT.equals(token)) { + return LocalPortElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LogElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LogElement.java new file mode 100644 index 00000000000..ac58807305e --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LogElement.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * Represents a http request or response element. + * + * @author croudet + * @since 2.0 + */ +public interface LogElement { + /** + * Events. + */ + enum Event { + ON_REQUEST_HEADERS, ON_RESPONSE_HEADERS, ON_RESPONSE_WRITE, ON_LAST_RESPONSE_WRITE; + + public static final Set REQUEST_HEADERS_EVENTS = Collections.unmodifiableSet(EnumSet.of(Event.ON_REQUEST_HEADERS)); + public static final Set RESPONSE_HEADERS_EVENTS = Collections.unmodifiableSet(EnumSet.of(Event.ON_RESPONSE_HEADERS)); + } + + /** + * The sets of events that this log element must process. Empty for ConstantElement. + * @return A list of events. + */ + Set events(); + + /** + * Reset the computed value. + */ + default void reset() { + + } + + /** + * Responds to an ON_REQUEST_HEADERS event. + * Also used for ConstantElement with all parameters as null. + * + * @param channel The socket channel. + * @param method The http method. + * @param headers The request headers. + * @param uri The request uri. + * @param protocol The request protocol. + * @return The processed value. + */ + default String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return ConstantElement.UNKNOWN_VALUE; + } + + /** + * Responds to an ON_RESPONSE_HEADERS event. + * + * @param ctx The ChannelHandlerContext. + * @param headers The response headers. + * @param status The response status. + * @return The processed value. + */ + default String onResponseHeaders(ChannelHandlerContext ctx, HttpHeaders headers, String status) { + return ConstantElement.UNKNOWN_VALUE; + } + + /** + * Responds to an ON_RESPONSE_WRITE event. + * + * @param bytesSent The number of bytes sent. + */ + default void onResponseWrite(int bytesSent) { + + } + + /** + * Responds to an ON_LAST_RESPONSE_WRITE event. + * + * @param bytesSent The number of bytes sent. + * @return The processed value. + */ + default String onLastResponseWrite(int bytesSent) { + return ConstantElement.UNKNOWN_VALUE; + } + + /** + * Copy this log element when it is not stateless. + * @return A copy of this log element. + */ + LogElement copy(); +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LogElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LogElementBuilder.java new file mode 100644 index 00000000000..58525a8714f --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/LogElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for LogElement. + * + * @author croudet + * @since 2.0 + */ +public interface LogElementBuilder { + + /** + * Builds the log element for the specified token. It should return null it the token is not supported. + * + * @param token The log element marker. + * @param param An optional paramter. + * @return A LogElement or null if not supported by the builder. + */ + LogElement build(String token, String param); +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteHostElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteHostElement.java new file mode 100644 index 00000000000..dee76392207 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteHostElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * RemoteHostElement LogElement. + * + * @author croudet + * @since 2.0 + */ +final class RemoteHostElement implements LogElement { + /** + * The remote host marker. + */ + public static final String REMOTE_HOST = "h"; + + /** + * The RemoteHostElement instance. The remote Host address (if resolved.) + */ + static final RemoteHostElement INSTANCE = new RemoteHostElement(); + + private RemoteHostElement() { + + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return channel.remoteAddress().getAddress().getHostName(); + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String toString() { + return '%' + REMOTE_HOST; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteHostElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteHostElementBuilder.java new file mode 100644 index 00000000000..8f77d53f095 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteHostElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for RemoteHostElement. + * + * @author croudet + * @since 2.0 + */ +public final class RemoteHostElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (RemoteHostElement.REMOTE_HOST.equals(token)) { + return RemoteHostElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteIpElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteIpElement.java new file mode 100644 index 00000000000..982a212b4f7 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteIpElement.java @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Locale; +import java.util.Set; + +import io.micronaut.http.HttpHeaders; +import io.netty.channel.socket.SocketChannel; + +/** + * RemoteIpElement LogElement. The remote IP address. + * + * @author croudet + * @since 2.0 + */ +final class RemoteIpElement implements LogElement { + /** + * The remote ip marker. + */ + public static final String REMOTE_IP = "a"; + + /** The HTTP {@code X-Forwarded-For} header field name (superseded by {@code Forwarded}). */ + public static final String X_FORWARDED_FOR = "X-Forwarded-For"; + + /** + * The RemoteIpElement instance. + */ + static final RemoteIpElement INSTANCE = new RemoteIpElement(); + + private RemoteIpElement() { + + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, io.netty.handler.codec.http.HttpHeaders headers, String uri, String protocol) { + // maybe this request was proxied or load balanced. + // try and get the real originating IP + final String xforwardedFor = headers.get(X_FORWARDED_FOR, null); + if (xforwardedFor == null) { + final String forwarded = headers.get(HttpHeaders.FORWARDED, null); + if (forwarded != null) { + String inet = processForwarded(forwarded); + if (inet != null) { + return inet; + } + } + } else { + return processXForwardedFor(xforwardedFor); + } + return channel.remoteAddress().getAddress().getHostAddress(); + } + + private static String processXForwardedFor(String xforwardedFor) { + // can contain multiple IPs for proxy chains. the first ip is our + // client. + final int firstComma = xforwardedFor.indexOf(','); + if (firstComma >= 0) { + return xforwardedFor.substring(0, firstComma); + } else { + return xforwardedFor; + } + } + + private static String processForwarded(String forwarded) { + final int firstComma = forwarded.indexOf(','); + final String firstForward = (firstComma >= 0 ? forwarded.substring(0, firstComma) : forwarded) + .toLowerCase(Locale.US); + int startIndex = firstForward.indexOf("for"); + if (startIndex == -1) { + return null; + } + final int semiColonIndex = firstForward.indexOf(';'); + final int endIndex = semiColonIndex >= 0 ? semiColonIndex : firstForward.length(); + // skip 'for=' + startIndex += 4; + // consume space and '=' + while (startIndex < endIndex) { + char c = firstForward.charAt(startIndex); + if (Character.isWhitespace(c) || c == '=') { + ++startIndex; + } else { + return firstForward.substring(startIndex, endIndex); + } + } + return null; + } + + @Override + public String toString() { + return '%' + REMOTE_IP; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteIpElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteIpElementBuilder.java new file mode 100644 index 00000000000..30a36f6306e --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RemoteIpElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for RemoteIpElement. + * + * @author croudet + * @since 2.0 + */ +public final class RemoteIpElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (RemoteIpElement.REMOTE_IP.equals(token)) { + return RemoteIpElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestLineElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestLineElement.java new file mode 100644 index 00000000000..04841fa5c46 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestLineElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * RequestLineElement LogElement. The request line. + * + * @author croudet + * @since 2.0 + */ +final class RequestLineElement implements LogElement { + /** + * The request line marker. + */ + public static final String REQUEST_LINE = "r"; + + /** + * The RequestLineElement instance. + */ + static final RequestLineElement INSTANCE = new RequestLineElement(); + + private RequestLineElement() { + + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return method + ' ' + uri + ' ' + protocol; + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String toString() { + return '%' + REQUEST_LINE; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestLineElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestLineElementBuilder.java new file mode 100644 index 00000000000..b16426e4483 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestLineElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for RequestLineElement. + * + * @author croudet + * @since 2.0 + */ +public final class RequestLineElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (RequestLineElement.REQUEST_LINE.equals(token)) { + return RequestLineElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestMethodElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestMethodElement.java new file mode 100644 index 00000000000..d7a79f6531a --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestMethodElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * RequestMethodElement LogElement. The request method. + * + * @author croudet + * @since 2.0 + */ +final class RequestMethodElement implements LogElement { + /** + * The request method marker. + */ + public static final String REQUEST_METHOD = "m"; + + /** + * The RequestMethodElement instance. + */ + static final RequestMethodElement INSTANCE = new RequestMethodElement(); + + private RequestMethodElement() { + + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return method; + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String toString() { + return '%' + REQUEST_METHOD; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestMethodElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestMethodElementBuilder.java new file mode 100644 index 00000000000..2ac77ccae2f --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestMethodElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for RequestMethodElement. + * + * @author croudet + * @since 2.0 + */ +public final class RequestMethodElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (RequestMethodElement.REQUEST_METHOD.equals(token)) { + return RequestMethodElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestProtocolElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestProtocolElement.java new file mode 100644 index 00000000000..e5bd2e26a5e --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestProtocolElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * RequestProtocolElement LogElement. The request protocol. + * + * @author croudet + * @since 2.0 + */ +final class RequestProtocolElement implements LogElement { + /** + * The request protocol marker. + */ + public static final String REQUEST_PROTOCOL = "H"; + + /** + * The RequestProtocolElement instance. + */ + static final RequestProtocolElement INSTANCE = new RequestProtocolElement(); + + private RequestProtocolElement() { + + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return protocol; + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String toString() { + return 'H' + REQUEST_PROTOCOL; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestProtocolElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestProtocolElementBuilder.java new file mode 100644 index 00000000000..d98de82b23e --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestProtocolElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for RequestProtocolElement. + * + * @author croudet + * @since 2.0 + */ +public final class RequestProtocolElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (RequestProtocolElement.REQUEST_PROTOCOL.equals(token)) { + return RequestProtocolElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestUriElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestUriElement.java new file mode 100644 index 00000000000..94214305bd1 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestUriElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * RequestUriElement LogElement. The request uri. + * + * @author croudet + * @since 2.0 + */ +final class RequestUriElement implements LogElement { + /** + * The request uri marker. + */ + public static final String REQUEST_URI = "x"; + + /** + * The RequestUriElement instance. + */ + static final RequestUriElement INSTANCE = new RequestUriElement(); + + private RequestUriElement() { + + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) { + return uri; + } + + @Override + public Set events() { + return Event.REQUEST_HEADERS_EVENTS; + } + + @Override + public String toString() { + return '%' + REQUEST_URI; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestUriElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestUriElementBuilder.java new file mode 100644 index 00000000000..0ae8a1cce63 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/RequestUriElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for RequestUriElement. + * + * @author croudet + * @since 2.0 + */ +public final class RequestUriElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (RequestUriElement.REQUEST_URI.equals(token)) { + return RequestUriElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ResponseCodeElement.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ResponseCodeElement.java new file mode 100644 index 00000000000..041877b8779 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ResponseCodeElement.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +import java.util.Set; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaders; + +/** + * ResponseCodeElement LogElement. The response code. + * + * @author croudet + * @since 2.0 + */ +final class ResponseCodeElement implements LogElement { + /** + * The response code marker. + */ + public static final String RESPONSE_CODE = "s"; + + /** + * The ResponseCodeElement instance. + */ + static final ResponseCodeElement INSTANCE = new ResponseCodeElement(); + + private ResponseCodeElement() { + + } + + @Override + public LogElement copy() { + return this; + } + + @Override + public String onResponseHeaders(ChannelHandlerContext ctx, HttpHeaders headers, String status) { + return status; + } + + @Override + public Set events() { + return Event.RESPONSE_HEADERS_EVENTS; + } + + @Override + public String toString() { + return '%' + RESPONSE_CODE; + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ResponseCodeElementBuilder.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ResponseCodeElementBuilder.java new file mode 100644 index 00000000000..fccecf4a568 --- /dev/null +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/element/ResponseCodeElementBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element; + +/** + * Builder for ResponseCodeElement. + * + * @author croudet + * @since 2.0 + */ +public final class ResponseCodeElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (ResponseCodeElement.RESPONSE_CODE.equals(token)) { + return ResponseCodeElement.INSTANCE; + } + return null; + } + +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java index 8a48b5dd655..64a613b8e02 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/websocket/NettyServerWebSocketUpgradeHandler.java @@ -39,6 +39,7 @@ import io.micronaut.websocket.context.WebSocketBeanRegistry; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; @@ -187,6 +188,10 @@ public Publisher> proceed(io.micronaut.http.HttpRequest { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParserSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParserSpec.groovy new file mode 100644 index 00000000000..84f9abab694 --- /dev/null +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/handler/accesslog/element/AccessLogFormatParserSpec.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.netty.handler.accesslog.element + +import spock.lang.Specification + +class AccessLogFormatParserSpec extends Specification { + + def "test access log format parser for predefined formats"() { + given: + AccessLogFormatParser parser = new AccessLogFormatParser(null); + + expect: + parser.toString() == "%h - - %t \"%r\" %s %b" + + when: + parser = new AccessLogFormatParser(AccessLogFormatParser.COMMON_LOG_FORMAT) + + then: + parser.toString() == "%h - - %t \"%r\" %s %b" + + when: + parser = new AccessLogFormatParser(AccessLogFormatParser.COMBINED_LOG_FORMAT) + + then: + parser.toString() == "%h - - %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"" + } + + def "test access log format parser for custom formats"() { + given: + AccessLogFormatParser parser = new AccessLogFormatParser("%h %l %u %t \"%r\" %s %b %% some string"); + + expect: + parser.toString() == "%h - - %t \"%r\" %s %b %% some string" + + when: + parser = new AccessLogFormatParser("%h %l %u %{'['dd.MM.yyyy']'}t \"%r\" %s %b") + + then: + parser.toString() == "%h - - %{'['dd.MM.yyyy']'}t \"%r\" %s %b" + + when: + parser = new AccessLogFormatParser("%h %l %u %t \"%r\" %s %b \"%{cookie1}C\" \"%{cookie2}c\"") + + then: + parser.toString() == "%h - - %t \"%r\" %s %b \"%{cookie1}C\" \"%{cookie2}c\"" + } + + def "test access log format parser for invalid formats"() { + when: + AccessLogFormatParser parser = new AccessLogFormatParser("%h %l %u %t \"%r\" % %b"); + + then: + parser.toString() == "%h - - %t \"%r\" -%b" + + when: + parser = new AccessLogFormatParser("%h %l %u %t \"%r\" %z %b") + + then: + parser.toString() == "%h - - %t \"%r\" - %b" + + } + + +} diff --git a/session/build.gradle b/session/build.gradle index b41b8bdd8ad..83d1cf00b2b 100644 --- a/session/build.gradle +++ b/session/build.gradle @@ -7,7 +7,7 @@ dependencies { api project(":http") compileOnly project(":http-server") - + compileOnly project(":http-server-netty") testAnnotationProcessor project(":inject-java") testCompileOnly project(":inject-groovy") @@ -19,4 +19,4 @@ dependencies { } -//compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] \ No newline at end of file +//compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] diff --git a/session/src/main/java/io/micronaut/session/http/SessionLogElement.java b/session/src/main/java/io/micronaut/session/http/SessionLogElement.java new file mode 100644 index 00000000000..7ca92f559f9 --- /dev/null +++ b/session/src/main/java/io/micronaut/session/http/SessionLogElement.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.session.http; + +import io.micronaut.http.server.netty.handler.accesslog.element.ConstantElement; +import io.micronaut.http.server.netty.handler.accesslog.element.LogElement; + +import java.util.Set; + +import io.micronaut.http.server.netty.NettyHttpRequest; +import io.micronaut.session.Session; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; + +/** + * SessionLogElement LogElement. The session. + * + * @author croudet + * @since 2.0 + */ +public class SessionLogElement implements LogElement { + /** + * The session marker. + */ + public static final String SESSION = "u"; + + @SuppressWarnings("rawtypes") + private static final AttributeKey KEY = AttributeKey.valueOf(NettyHttpRequest.class.getSimpleName()); + + private final String property; + + /** + * Creates a SessionElement. + * + * @param property A property stored in the Session or null. When property is null the session id will be printed. + */ + SessionLogElement(String property) { + this.property = property; + } + + @Override + public LogElement copy() { + return this; + } + + @SuppressWarnings("rawtypes") + @Override + public String onResponseHeaders(ChannelHandlerContext ctx, HttpHeaders headers, String status) { + final Attribute attr = ctx.channel().attr(KEY); + NettyHttpRequest request = attr.get(); + if (request == null) { + return ConstantElement.UNKNOWN_VALUE; + } + return SessionForRequest.find(request).map(this::value).orElse(ConstantElement.UNKNOWN_VALUE); + } + + private String value(Session session) { + return property == null ? session.getId() : session.get(property).map(Object::toString).orElse(ConstantElement.UNKNOWN_VALUE); + } + + @Override + public Set events() { + return Event.RESPONSE_HEADERS_EVENTS; + } + + @Override + public String toString() { + return '%' + SESSION; + } +} diff --git a/session/src/main/java/io/micronaut/session/http/SessionLogElementBuilder.java b/session/src/main/java/io/micronaut/session/http/SessionLogElementBuilder.java new file mode 100644 index 00000000000..86817547479 --- /dev/null +++ b/session/src/main/java/io/micronaut/session/http/SessionLogElementBuilder.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.session.http; + +import io.micronaut.http.server.netty.handler.accesslog.element.LogElement; +import io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder; + +/** + * Builder for SessionLogElement. + * + * @author croudet + * @since 2.0 + */ +public final class SessionLogElementBuilder implements LogElementBuilder { + + @Override + public LogElement build(String token, String param) { + if (SessionLogElement.SESSION.equals(token)) { + return new SessionLogElement(param); + } + return null; + } + +} diff --git a/session/src/main/resources/META-INF/services/io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder b/session/src/main/resources/META-INF/services/io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder new file mode 100644 index 00000000000..6fb005a2f7c --- /dev/null +++ b/session/src/main/resources/META-INF/services/io.micronaut.http.server.netty.handler.accesslog.element.LogElementBuilder @@ -0,0 +1 @@ +io.micronaut.session.http.SessionLogElementBuilder diff --git a/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc new file mode 100644 index 00000000000..1f4c7ca02ba --- /dev/null +++ b/src/main/docs/guide/httpServer/serverConfiguration/accessLogger.adoc @@ -0,0 +1,85 @@ +In the spirit of http://httpd.apache.org/docs/current/mod/mod_log_config.html[apache mod_log_config] and https://tomcat.apache.org/tomcat-10.0-doc/config/valve.html#Access_Logging[Tomcat Access Log Valve], it is possible to enabled an access logger for + the http server (it works for both http/1 and http/2.) + +To enable and configure the access logger, in `application.yml` set: + +.Enabling the access logger +[source,yaml] +---- +micronaut: + server: + netty: + access-logger: + enabled: true # Enables the access logger + logger-name: my-access-logger # A logger name, default is `HTTP_ACCESS_LOGGER` + log-format: common # A log format, default is Common Log Format +---- + +==== Logback Configuration + +In addition to enabling the access logger, you have to add a logger for the specified or default logger name. +For instance using the default logger name for logback: + +.Logback configuration +[source,xml] +---- + + true + log/http-access.log + + + log/http-access-%d{yyyy-MM-dd}.log + + 7 + + + UTF-8 + %msg%n + + true + + + + + +---- + +The pattern should only have the message marker as other elements will be processed by the access logger. + +==== Log Format + +The syntax is based on http://httpd.apache.org/docs/current/mod/mod_log_config.html[Apache httpd log format]. + +Here are the supported markers: + +* *%a* - Remote IP address +* *%A* - Local IP address +* *%b* - Bytes sent, excluding HTTP headers, or '-' if no bytes were sent +* *%B* - Bytes sent, excluding HTTP headers +* *%h* - Remote host name +* *%H* - Request protocol +* *%{
}i* - Request header. If the argument is omitted (*%i*) all headers will be printed +* *%{
}o* - Response header. If the argument is omitted (*%o*) all headers will be printed +* *%{}C* - Request cookie (COOKIE). If the argument is omitted (*%C*) all cookies will be printed +* *%{}c* - Response cookie (SET_COOKIE). If the argument is omitted (*%c*) all cookies will be printed +* *%l* - Remote logical username from identd (always returns '-') +* *%m* - Request method +* *%p* - Local port +* *%q* - Query string (excluding the '?' character) +* *%r* - First line of the request +* *%s* - HTTP status code of the response +* *%{}t* - Date and time. If the argument is ommitted the Common Log Format format is used ("'['dd/MMM/yyyy:HH:mm:ss Z']'"). +* If the format starts with begin: (default) the time is taken at the beginning of the request processing. If it starts with end: it is the time when the log entry gets written, close to the end of the request processing. +* The format should follow the DateTimeFormatter syntax. +* *%{property}u* - Remote user that was authenticated. When *micronaut-session* is on the classpath, returns the session id if the argument is omitted or the specified property otherwise prints '-' +* *%U* - Requested URI +* *%v* - Local server name +* *%D* - Time taken to process the request, in millis +* *%T* - Time taken to process the request, in seconds + +In addition, the following aliases for commonly utilized patterns: + +* *common* - %h %l %u %t "%r" %s %b Common Log Format (CLF) +* *combined* - %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" Combined Log Format diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index ae39dea0f55..6d9c4f5fd01 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -110,6 +110,7 @@ httpServer: cors: Configuring CORS https: Securing the Server with HTTPS dualProtocol: Enabling HTTP and HTTPS + accessLogger: Enabling Access Logger views: title: Server Side View Rendering openapi: OpenAPI / Swagger Support diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 329db707aa4..89f64c86c4b 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -65,6 +65,7 @@ dependencies { testImplementation 'io.netty:netty-tcnative-boringssl-static:2.0.29.Final' testImplementation "io.netty:netty-tcnative-boringssl-static:2.0.29.Final:${Os.isFamily(Os.FAMILY_MAC) ? 'osx-x86_64' : 'linux-x86_64'}" testImplementation 'org.zalando:logbook-netty:2.1.0' + testImplementation "ch.qos.logback:logback-classic:1.2.3" } //tasks.withType(Test) { diff --git a/test-suite/src/test/groovy/io/micronaut/http/HttpAccessLogger.groovy b/test-suite/src/test/groovy/io/micronaut/http/HttpAccessLogger.groovy new file mode 100644 index 00000000000..141dbde7c49 --- /dev/null +++ b/test-suite/src/test/groovy/io/micronaut/http/HttpAccessLogger.groovy @@ -0,0 +1,208 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http + +import io.micronaut.context.annotation.Property +import io.micronaut.core.convert.format.Format +import io.micronaut.core.type.Argument +import io.micronaut.http.* +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.QueryValue +import io.micronaut.http.client.RxHttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http2.Http2AccessLoggerSpec.MemoryAppender +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.session.Session +import io.micronaut.session.annotation.SessionValue +import io.micronaut.test.annotation.MicronautTest +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Single +import io.reactivex.functions.Consumer +import spock.lang.Issue +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import javax.annotation.Nullable +import javax.inject.Inject + +import org.slf4j.LoggerFactory + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase + +import java.time.LocalDate +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit + +/** + * @author Christophe Roudet + * @since 2.0 + */ +@MicronautTest +@Property(name = "micronaut.server.netty.log-level", value = 'trace') +@Property(name = "micronaut.http.client.log-level", value = 'trace') +@Property(name = 'micronaut.server.netty.access-logger.enabled', value = 'true') +class HttpAccessLoggerSpec extends Specification { + + @Inject + @Client("/") + RxHttpClient client + + static MemoryAppender appender = new MemoryAppender() + + static { + Logger l = (Logger) LoggerFactory.getLogger("HTTP_ACCESS_LOGGER") + l.addAppender(appender) + appender.start() + } + + + @Inject + EmbeddedServer embeddedServer + + void "test simple get request with type - access logger"() { + when: + appender.events.clear() + Flowable> flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.GET("/get/simple"), String + )) + HttpResponse response = flowable.blockingFirst() + def body = response.getBody() + + + then: + response.status == HttpStatus.OK + body.isPresent() + body.get() == 'success' + appender.headLog(10).contains("" + HttpStatus.OK.getCode()) + } + + void "test simple 404 request - access logger"() { + + when: + appender.events.clear() + def flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.GET("/get/doesntexist") + )) + flowable.blockingFirst() + + then: + def e = thrown(HttpClientResponseException) + e.message == "Page Not Found" + e.status == HttpStatus.NOT_FOUND + appender.headLog(10).contains("" + HttpStatus.NOT_FOUND.getCode()) + } + + void "test 500 request with body - access logger"() { + + when: + appender.events.clear() + def flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.GET("/get/error"), Argument.of(String), Argument.of(String) + )) + flowable.blockingFirst() + + then: + def e = thrown(HttpClientResponseException) + e.message == "Server error" + e.status == HttpStatus.INTERNAL_SERVER_ERROR + e.response.getBody(String).get() == "Server error" + appender.headLog(10).contains("" + HttpStatus.INTERNAL_SERVER_ERROR.getCode()) + + } + + void "test simple session - access logger"() { + when: + appender.events.clear() + Flowable> flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.GET("/sessiontest/simple"), String + )) + HttpResponse response = flowable.blockingFirst() + + then: + response.getBody().get() == "not in session" + response.header(HttpHeaders.AUTHORIZATION_INFO) + // host - - [25/May/2020:15:14:00 -0400] "GET /get/simple HTTP/1.1" 200 7 + // host - f9d1c6b2-2980-4e6a-826c-bdfc6a21417c [25/May/2020:15:14:00 -0400] "GET /sessiontest/simple HTTP/1.1" 200 14 + !appender.headLog(10).contains(" - - [") + + when: + def sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) + flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.GET("/sessiontest/simple") + .header(HttpHeaders.AUTHORIZATION_INFO, sessionId) + , String + )) + response = flowable.blockingFirst() + + then: + response.getBody().get() == "value in session" + response.header(HttpHeaders.AUTHORIZATION_INFO) + !appender.headLog(10).contains(" - - [") + } + + @Controller("/get") + static class GetController { + + @Get(value = "/simple", produces = MediaType.TEXT_PLAIN) + String simple() { + return "success" + } + + @Get(value = "/error", produces = MediaType.TEXT_PLAIN) + HttpResponse error() { + return HttpResponse.serverError().body("Server error") + } + + } + + @Controller('/sessiontest') + static class SessionController { + + @Get("/simple") + String simple(Session session) { + return session.get("myValue").orElseGet({ + session.put("myValue", "value in session") + "not in session" + }) + } + + } + + private static class MemoryAppender extends AppenderBase { + private final BlockingQueue events = new LinkedBlockingQueue<>() + + @Override + protected void append(ILoggingEvent e) { + events.add(e.formattedMessage) + } + + public Queue getEvents() { + return events + } + + public String headLog(long timeout) { + return events.poll(timeout, TimeUnit.SECONDS) + } + } + +} diff --git a/test-suite/src/test/groovy/io/micronaut/http2/Http2AccessLogger.groovy b/test-suite/src/test/groovy/io/micronaut/http2/Http2AccessLogger.groovy new file mode 100644 index 00000000000..90b560de733 --- /dev/null +++ b/test-suite/src/test/groovy/io/micronaut/http2/Http2AccessLogger.groovy @@ -0,0 +1,191 @@ +package io.micronaut.http2 + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import ch.qos.logback.classic.Logger +import org.slf4j.LoggerFactory + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.docs.server.json.Person +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Produces +import io.micronaut.http.client.RxHttpClient +import io.micronaut.http.client.RxStreamingHttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.sse.Event +import io.micronaut.runtime.server.EmbeddedServer +import io.reactivex.Flowable +import org.reactivestreams.Publisher +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import java.time.Duration +import java.time.temporal.ChronoUnit +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit + +// Netty + HTTP/2 on JDKs less than 9 require tcnative setup +// which is not included in this test suite +//@IgnoreIf({ !Jvm.current.isJava9Compatible() }) +class Http2AccessLoggerSpec extends Specification { + @Shared @AutoCleanup EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ + 'micronaut.ssl.enabled': true, + "micronaut.server.http-version" : "2.0", + "micronaut.http.client.http-version" : "2.0", + 'micronaut.ssl.buildSelfSigned': true, + 'micronaut.ssl.port': -1, + "micronaut.http.client.log-level" : "TRACE", + "micronaut.server.netty.log-level" : "TRACE", + 'micronaut.server.netty.access-logger.enabled': true + ]) + RxHttpClient client = server.getApplicationContext().getBean(RxHttpClient) + static MemoryAppender appender = new MemoryAppender() + + static { + Logger l = (Logger) LoggerFactory.getLogger("HTTP_ACCESS_LOGGER") + l.addAppender(appender) + appender.start() + } + + void "test make HTTP/2 stream request with access logger enabled"() { + when:"A non stream request is executed" + appender.events.clear() + def people = ((RxStreamingHttpClient)client).jsonStream(HttpRequest.GET("${server.URL}/http2/personStream"), Person) + .toList().blockingGet() + + then: + people + appender.headLog(10) + + when:"posting a data" + def response = client.exchange(HttpRequest.POST("${server.URL}/http2/personStream", Object), Argument.listOf(Person)) + .blockingFirst() + + then: + response + appender.headLog(10) + + } + + void "test make HTTP/2 sse stream request with access logger enabled"() { + when:"An sse stream is obtain" + def client = server.applicationContext.getBean(TestHttp2Client) + appender.events.clear() + def results = client.rich().toList().blockingGet() + + then: + results.size() == 4 + appender.headLog(10) + } + + void "test make HTTP/2 request with access logger enabled - HTTPS"() { + when: + appender.events.clear() + def result = client.retrieve("${server.URL}/http2").blockingFirst() + + then: + result == 'Version: HTTP_2_0' + appender.headLog(10) + + when:"operation repeated to use same connection" + result = client.retrieve("${server.URL}/http2").blockingFirst() + + then: + result == 'Version: HTTP_2_0' + appender.headLog(10) + + when:"A non stream request is executed" + client.retrieve(HttpRequest.GET("${server.URL}/http2/personStream"), Argument.listOf(Person)) + .blockingFirst() + + then: + appender.headLog(10) + } + + void "test make HTTP/2 request with access logger enabled - upgrade over HTTP"() { + given: + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ + "micronaut.server.http-version" : "2.0", + "micronaut.http.client.http-version" : "2.0", + "micronaut.http.client.read-timeout": -1, + "micronaut.http.client.log-level" : "TRACE", + "micronaut.server.netty.log-level" : "TRACE", + 'micronaut.server.netty.access-logger.enabled': true + ]) + RxHttpClient client = server.getApplicationContext().getBean(RxHttpClient) + appender.events.clear() + + when: + def result = client.retrieve("${server.URL}/http2").blockingFirst() + + then: + result == 'Version: HTTP_2_0' + appender.headLog(10) + + when:"operation repeated to use same connection" + result = client.retrieve("${server.URL}/http2").blockingFirst() + + then: + result == 'Version: HTTP_2_0' + appender.headLog(10) + + cleanup: + server.close() + } + + void "test HTTP/2 server with HTTP/1 client request works with access logger enabled"() { + given: + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, [ + 'micronaut.ssl.enabled': true, + "micronaut.server.http-version" : "2.0", + 'micronaut.ssl.buildSelfSigned': true, + 'micronaut.ssl.port': -1, + "micronaut.http.client.log-level" : "TRACE", + "micronaut.server.netty.log-level" : "TRACE", + 'micronaut.server.netty.access-logger.enabled': true + ]) + RxHttpClient client = server.getApplicationContext().getBean(RxHttpClient) + appender.events.clear() + def result = client.retrieve("${server.URL}/http2").blockingFirst() + + expect: + result == 'Version: HTTP_1_1' + appender.headLog(10) + + cleanup: + server.close() + } + + private static class MemoryAppender extends AppenderBase { + private final BlockingQueue events = new LinkedBlockingQueue<>() + + @Override + protected void append(ILoggingEvent e) { + events.add(e.formattedMessage) + } + + public Queue getEvents() { + return events + } + + public String headLog(long timeout) { + return events.poll(timeout, TimeUnit.SECONDS) + } + } + + + @Client('/http2/sse') + static interface TestHttp2Client { + + @Get(value = '/rich', processes = MediaType.TEXT_EVENT_STREAM) + Flowable> rich() + } +}