diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index ed6bb5e0fdfcc..eb34bef8f74e5 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -218,31 +218,43 @@ link:http://undertow.io/undertow-docs/undertow-docs-2.0.0/index.html#predicates- === Configuring HTTP Access Logs -You can add HTTP request logging by configuring the `AccessHandler` in the `undertow-handlers.conf` file. +You can add HTTP request logging by configuring it in `application.properties`. There are two options for logging, +either logging to the standard JBoss logging output, or logging to a dedicated file. -The simplest possible configuration can be a standard Apache `common` Log Format: +include: quarkus-vertx-http-config-group-access-log-config.adoc -[source] ----- -access-log('common') ----- - -This will log every request using the standard Quarkus logging infrastructure under the `io.undertow.accesslog` category. - -You can customize the category like this: - - -[source] ----- -access-log(format='common', category='my.own.category') ----- - -Finally the logging format can be customized: - -[source] ----- -access-log(format='%h %l %u %t "%r" %s %b %D "%{i,Referer}" "%{i,User-Agent}" "%{i,X-Request-ID}"', category='my.own.category') ----- +[frame="topbot",options="header"] +|=== +|Attribute |Short Form|Long Form +|Remote IP address | `%a` | `%{REMOTE_IP}` +|Local IP address | `%A` | `%{LOCAL_IP}` +|Bytes sent, excluding HTTP headers, or '-' if no bytes were sent | `%b` | +|Bytes sent, excluding HTTP headers | `%B` | `%{BYTES_SENT}` +|Remote host name | `%h` | `%{REMOTE_HOST}` +|Request protocol | `%H` | `%{PROTOCOL}` +|Request method | `%m` | `%{METHOD}` +|Local port | `%p` | `%{LOCAL_PORT}` +|Query string (prepended with a '?' if it exists, otherwise an empty string) | `%q` | `%{QUERY_STRING}` +|First line of the request | `%r` | `%{REQUEST_LINE}` +|HTTP status code of the response | `%s` | `%{RESPONSE_CODE}` +|Date and time, in Common Log Format format | `%t` | `%{DATE_TIME}` +|Remote user that was authenticated | `%u` | `%{REMOTE_USER}` +|Requested URL path | `%U` | `%{REQUEST_URL}` +|Request relative path | `%R` | `%{RELATIVE_PATH}` +|Local server name | `%v` | `%{LOCAL_SERVER_NAME}` +|Time taken to process the request, in millis | `%D` | `%{RESPONSE_TIME}` +|Time taken to process the request, in seconds | `%T` | +|Time taken to process the request, in micros | | `%{RESPONSE_TIME_MICROS}` +|Time taken to process the request, in nanos | | `%{RESPONSE_TIME_NANOS}` +|Current request thread name | `%I` | `%{THREAD_NAME}` +|SSL cypher | | `%{SSL_CIPHER}` +|SSL client certificate | | `%{SSL_CLIENT_CERT}` +|SSL session id | | `%{SSL_SESSION_ID}` +|Cookie value | | `%{c,cookie_name}` +|Query parameter | | `%{q,query_param_name}` +|Request header | | `%{i,request_header_name}` +|Response header | | `%{o,response_header_name}` +|=== === web.xml diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 8845860d438cd..4732a403bfdeb 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -17,6 +17,7 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationStartBuildItem; +import io.quarkus.deployment.builditem.ExecutorBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; @@ -119,8 +120,8 @@ ServiceStartBuildItem finalizeRouter( BodyHandlerBuildItem bodyHandlerBuildItem, BuildProducer shutdownListenerBuildItemBuildProducer, ShutdownConfig shutdownConfig, - CoreVertxBuildItem core // Injected to be sure that Vert.x has been produced before calling this method. - ) + CoreVertxBuildItem core, // Injected to be sure that Vert.x has been produced before calling this method. + ExecutorBuildItem executorBuildItem) throws BuildException, IOException { Optional defaultRoute; if (defaultRoutes == null || defaultRoutes.isEmpty()) { @@ -149,7 +150,7 @@ ServiceStartBuildItem finalizeRouter( defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), listOfFilters, vertx.getVertx(), router.getRouter(), httpBuildTimeConfig.rootPath, launchMode.getLaunchMode(), !requireBodyHandlerBuildItems.isEmpty(), bodyHandler, httpConfiguration, gracefulShutdownFilter, - shutdownConfig); + shutdownConfig, executorBuildItem.getExecutorProxy()); return new ServiceStartBuildItem("vertx-http"); } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/accesslog/AccessLogFileTestCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/accesslog/AccessLogFileTestCase.java new file mode 100644 index 0000000000000..6e8e98e8a4a5f --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/accesslog/AccessLogFileTestCase.java @@ -0,0 +1,112 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * 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.quarkus.vertx.http.accesslog; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.awaitility.Awaitility; +import org.awaitility.core.ThrowingRunnable; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.ByteArrayAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.util.IoUtils; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +/** + * Tests writing the access log to a file + * + * @author Stuart Douglas + */ +public class AccessLogFileTestCase { + + @RegisterExtension + public static QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(new Supplier() { + @Override + public JavaArchive get() { + Path logDirectory; + try { + logDirectory = Files.createTempDirectory("quarkus-tests"); + //backslash is an escape char, we need this to be properly formatted for windows + Properties p = new Properties(); + p.setProperty("quarkus.http.access-log.enabled", "true"); + p.setProperty("quarkus.http.access-log.log-to-file", "true"); + p.setProperty("quarkus.http.access-log.base-file-name", "server"); + p.setProperty("quarkus.http.access-log.log-directory", logDirectory.toAbsolutePath().toString()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + p.store(out, null); + + return ShrinkWrap.create(JavaArchive.class) + .add(new ByteArrayAsset(out.toByteArray()), + "application.properties"); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + + @ConfigProperty(name = "quarkus.http.access-log.log-directory") + Path logDirectory; + + @BeforeEach + public void before() throws IOException { + Files.createDirectories(logDirectory); + } + + @AfterEach + public void after() throws IOException { + IoUtils.recursiveDelete(logDirectory); + } + + @Test + public void testSingleLogMessageToFile() throws IOException, InterruptedException { + RestAssured.get("/does-not-exist"); + + Awaitility.given().pollInterval(100, TimeUnit.MILLISECONDS) + .atMost(10, TimeUnit.SECONDS) + .untilAsserted(new ThrowingRunnable() { + @Override + public void run() throws Throwable { + try (Stream files = Files.list(logDirectory)) { + Assertions.assertEquals(1, (int) files.count()); + } + Path path = logDirectory.resolve("server.log"); + Assertions.assertTrue(Files.exists(path)); + String data = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + Assertions.assertTrue(data.contains("404")); + Assertions.assertTrue(data.contains("/does-not-exist")); + } + }); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AccessLogConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AccessLogConfig.java new file mode 100644 index 0000000000000..9bfe1a8975c3a --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AccessLogConfig.java @@ -0,0 +1,72 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class AccessLogConfig { + + /** + * If access logging is enabled. By default this will log via the standard logging facility + */ + @ConfigItem(defaultValue = "false") + public boolean enabled; + + /** + * The access log pattern: + * + * If this is the string 'common' or 'combined' then this will use one of the specified named formats: + * + * common: %h %l %u %t "%r" %s %b + * combined: %h %l %u %t "%r" %s %b "%{i,Referer}" "%{i,User-Agent}" + * + * Otherwise consult the Quarkus documentation for the full list of variables that can be used. + * + */ + @ConfigItem(defaultValue = "common") + public String pattern; + + /** + * If logging should be done to a separate file. + */ + @ConfigItem(defaultValue = "false") + public boolean logToFile; + + /** + * The access log file base name, defaults to 'quarkus' which will give a log file + * name of 'quarkus.log'. + * + */ + @ConfigItem(defaultValue = "quarkus") + public String baseFileName; + + /** + * The log directory to use when logging access to a file + * + * If this is not set then the current working directory is used. + */ + @ConfigItem + public Optional logDirectory; + + /** + * The log file suffix + */ + @ConfigItem(defaultValue = ".log") + public String logSuffix; + + /** + * The log category to use if logging is being done via the standard log mechanism (i.e. if base-file-name is empty). + * + */ + @ConfigItem(defaultValue = "io.quarkus.http.access-log") + public String category; + + /** + * If the log should be rotated daily + */ + @ConfigItem(defaultValue = "true") + public boolean rotate; + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index bcac90d554034..da370e15026aa 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -180,6 +180,16 @@ public class HttpConfiguration { @ConfigItem(defaultValue = "false") public boolean domainSocketEnabled; + /** + * If this is true then the request start time will be recorded to enable logging of total request time. + * + * This has a small performance penalty, so is disabled by default. + */ + @ConfigItem + public boolean recordRequestStartTime; + + AccessLogConfig accessLog; + public int determinePort(LaunchMode launchMode) { return launchMode == LaunchMode.TEST ? testPort : port; } @@ -188,7 +198,7 @@ public int determineSslPort(LaunchMode launchMode) { return launchMode == LaunchMode.TEST ? testSslPort : sslPort; } - public static enum InsecureRequests { + public enum InsecureRequests { ENABLED, REDIRECT, DISABLED; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 84218ebc9799f..6447cbf539982 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.runtime; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -12,6 +13,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -45,6 +47,11 @@ import io.quarkus.vertx.http.runtime.filters.Filter; import io.quarkus.vertx.http.runtime.filters.Filters; import io.quarkus.vertx.http.runtime.filters.GracefulShutdownFilter; +import io.quarkus.vertx.http.runtime.filters.QuarkusRequestWrapper; +import io.quarkus.vertx.http.runtime.filters.accesslog.AccessLogHandler; +import io.quarkus.vertx.http.runtime.filters.accesslog.AccessLogReceiver; +import io.quarkus.vertx.http.runtime.filters.accesslog.DefaultAccessLogReceiver; +import io.quarkus.vertx.http.runtime.filters.accesslog.JBossLoggingAccessLogReceiver; import io.vertx.core.AbstractVerticle; import io.vertx.core.AsyncResult; import io.vertx.core.Context; @@ -77,6 +84,11 @@ @Recorder public class VertxHttpRecorder { + /** + * The key that the request start time is stored under + */ + public static final String REQUEST_START_TIME = "io.quarkus.request-start-time"; + public static final String MAX_REQUEST_SIZE_KEY = "io.quarkus.max-request-size"; private static final Logger LOGGER = Logger.getLogger(VertxHttpRecorder.class.getName()); @@ -184,7 +196,8 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute List filterList, Supplier vertx, RuntimeValue runtimeValue, String rootPath, LaunchMode launchMode, boolean requireBodyHandler, Handler bodyHandler, HttpConfiguration httpConfiguration, - GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig) { + GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig, + Executor executor) { // install the default route at the end Router router = runtimeValue.getValue(); @@ -280,10 +293,37 @@ public void handle(HttpServerRequest event) { } }; } + boolean quarkusWrapperNeeded = false; if (shutdownConfig.isShutdownTimeoutSet()) { gracefulShutdownFilter.next(root); root = gracefulShutdownFilter; + quarkusWrapperNeeded = true; + } + + AccessLogConfig accessLog = httpConfiguration.accessLog; + if (accessLog.enabled) { + AccessLogReceiver receiver; + if (accessLog.logToFile) { + File outputDir = accessLog.logDirectory.isPresent() ? new File(accessLog.logDirectory.get()) : new File(""); + receiver = new DefaultAccessLogReceiver(executor, outputDir, accessLog.baseFileName, accessLog.logSuffix, + accessLog.rotate); + } else { + receiver = new JBossLoggingAccessLogReceiver(accessLog.category); + } + AccessLogHandler handler = new AccessLogHandler(receiver, accessLog.pattern, getClass().getClassLoader()); + router.route().order(Integer.MIN_VALUE).handler(handler); + quarkusWrapperNeeded = true; + } + + if (quarkusWrapperNeeded) { + Handler old = root; + root = new Handler() { + @Override + public void handle(HttpServerRequest event) { + old.handle(new QuarkusRequestWrapper(event)); + } + }; } Handler delegate = root; @@ -293,6 +333,15 @@ public void handle(HttpServerRequest event) { delegate.handle(new ResumingRequestWrapper(event)); } }; + if (httpConfiguration.recordRequestStartTime) { + router.route().order(Integer.MIN_VALUE).handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.put(REQUEST_START_TIME, System.nanoTime()); + event.next(); + } + }); + } rootHandler = root; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/BytesSentAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/BytesSentAttribute.java new file mode 100644 index 0000000000000..6914f4f26c673 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/BytesSentAttribute.java @@ -0,0 +1,59 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The bytes sent + * + */ +public class BytesSentAttribute implements ExchangeAttribute { + + public static final String BYTES_SENT_SHORT_UPPER = "%B"; + public static final String BYTES_SENT_SHORT_LOWER = "%b"; + public static final String BYTES_SENT = "%{BYTES_SENT}"; + + private final boolean dashIfZero; + + public BytesSentAttribute(boolean dashIfZero) { + this.dashIfZero = dashIfZero; + } + + @Override + public String readAttribute(final RoutingContext exchange) { + if (dashIfZero) { + long bytesSent = exchange.response().bytesWritten(); + return bytesSent == 0 ? "-" : Long.toString(bytesSent); + } else { + return Long.toString(exchange.response().bytesWritten()); + } + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Bytes sent", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Bytes Sent"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(BYTES_SENT_SHORT_LOWER)) { + return new BytesSentAttribute(true); + } + if (token.equals(BYTES_SENT) || token.equals(BYTES_SENT_SHORT_UPPER)) { + return new BytesSentAttribute(false); + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/CompositeExchangeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/CompositeExchangeAttribute.java new file mode 100644 index 0000000000000..5f3aa623ac6bc --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/CompositeExchangeAttribute.java @@ -0,0 +1,35 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * Exchange attribute that represents a combination of attributes that should be merged into a single string. + * + */ +public class CompositeExchangeAttribute implements ExchangeAttribute { + + private final ExchangeAttribute[] attributes; + + public CompositeExchangeAttribute(ExchangeAttribute[] attributes) { + ExchangeAttribute[] copy = new ExchangeAttribute[attributes.length]; + System.arraycopy(attributes, 0, copy, 0, attributes.length); + this.attributes = copy; + } + + @Override + public String readAttribute(RoutingContext exchange) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < attributes.length; ++i) { + final String val = attributes[i].readAttribute(exchange); + if (val != null) { + sb.append(val); + } + } + return sb.toString(); + } + + @Override + public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("combined", newValue); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ConstantExchangeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ConstantExchangeAttribute.java new file mode 100644 index 0000000000000..7ba4542690bb3 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ConstantExchangeAttribute.java @@ -0,0 +1,26 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * Exchange attribute that represents a fixed value + * + */ +public class ConstantExchangeAttribute implements ExchangeAttribute { + + private final String value; + + public ConstantExchangeAttribute(final String value) { + this.value = value; + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return value; + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("constant", newValue); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/CookieAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/CookieAttribute.java new file mode 100644 index 0000000000000..e15c4aacb5476 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/CookieAttribute.java @@ -0,0 +1,54 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.core.http.Cookie; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.impl.CookieImpl; + +/** + * A cookie + * + */ +public class CookieAttribute implements ExchangeAttribute { + + private final String cookieName; + + public CookieAttribute(final String cookieName) { + this.cookieName = cookieName; + } + + @Override + public String readAttribute(final RoutingContext exchange) { + Cookie cookie = exchange.getCookie(cookieName); + if (cookie == null) { + return null; + } + return cookie.getValue(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + exchange.response().addCookie(new CookieImpl(cookieName, newValue)); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Cookie"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{c,") && token.endsWith("}")) { + final String cookieName = token.substring(4, token.length() - 1); + return new CookieAttribute(cookieName); + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/DateTimeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/DateTimeAttribute.java new file mode 100644 index 0000000000000..b3ce6f4083785 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/DateTimeAttribute.java @@ -0,0 +1,96 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import io.vertx.ext.web.RoutingContext; + +/** + * The current time + * + */ +public class DateTimeAttribute implements ExchangeAttribute { + + private static final String COMMON_LOG_PATTERN = "[dd/MMM/yyyy:HH:mm:ss Z]"; + + private static final ThreadLocal COMMON_LOG_PATTERN_FORMAT = new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + SimpleDateFormat df = new SimpleDateFormat(COMMON_LOG_PATTERN, Locale.US); + return df; + } + }; + + public static final String DATE_TIME_SHORT = "%t"; + public static final String DATE_TIME = "%{DATE_TIME}"; + public static final String CUSTOM_TIME = "%{time,"; + + public static final ExchangeAttribute INSTANCE = new DateTimeAttribute(); + + private final String dateFormat; + private final ThreadLocal cachedFormat; + + private DateTimeAttribute() { + this.dateFormat = null; + this.cachedFormat = null; + } + + public DateTimeAttribute(final String dateFormat) { + this(dateFormat, null); + } + + public DateTimeAttribute(final String dateFormat, final String timezone) { + this.dateFormat = dateFormat; + this.cachedFormat = new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + final SimpleDateFormat format = new SimpleDateFormat(dateFormat); + if (timezone != null) { + format.setTimeZone(TimeZone.getTimeZone(timezone)); + } + return format; + } + }; + } + + @Override + public String readAttribute(final RoutingContext exchange) { + if (dateFormat == null) { + return COMMON_LOG_PATTERN_FORMAT.get().format(new Date()); + } else { + final SimpleDateFormat dateFormat = this.cachedFormat.get(); + return dateFormat.format(new Date()); + } + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Date time", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Date Time"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(DATE_TIME) || token.equals(DATE_TIME_SHORT)) { + return DateTimeAttribute.INSTANCE; + } + if (token.startsWith(CUSTOM_TIME) && token.endsWith("}")) { + return new DateTimeAttribute(token.substring(CUSTOM_TIME.length(), token.length() - 1)); + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttribute.java new file mode 100644 index 0000000000000..a306bb8d48b67 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttribute.java @@ -0,0 +1,27 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * Representation of a string attribute from a HTTP server exchange. + * + */ +public interface ExchangeAttribute { + + /** + * Resolve the attribute from the HTTP server exchange. This may return null if the attribute is not present. + * + * @param exchange The exchange + * @return The attribute + */ + String readAttribute(final RoutingContext exchange); + + /** + * Sets a new value for the attribute. Not all attributes are writable. + * + * @param exchange The exchange + * @param newValue The new value for the attribute + * @throws ReadOnlyAttributeException when attribute cannot be written + */ + void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeBuilder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeBuilder.java new file mode 100644 index 0000000000000..0adcd263f8da0 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeBuilder.java @@ -0,0 +1,34 @@ +package io.quarkus.vertx.http.runtime.attribute; + +/** + * An interface that knows how to build an exchange attribute from a textual representation. + *

+ * This makes it easy to configure attributes based on a string representation + * + */ +public interface ExchangeAttributeBuilder { + + /** + * The string representation of the attribute name. This is used solely for debugging / informational purposes + * + * @return The attribute name + */ + String name(); + + /** + * Build the attribute from a text based representation. If the attribute does not understand this representation then + * it will just return null. + * + * @param token The string token + * @return The exchange attribute, or null + */ + ExchangeAttribute build(final String token); + + /** + * The priority of the builder. Builders will be tried in priority builder. Built in builders use the priority range 0-100, + * + * @return The priority + */ + int priority(); + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeParser.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeParser.java new file mode 100644 index 0000000000000..fe91ce34eb45f --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeParser.java @@ -0,0 +1,170 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.ServiceLoader; + +import org.jboss.logging.Logger; + +/** + * Attribute parser for exchange attributes. This builds an attribute from a string definition. + *

+ * This uses a service loader mechanism to allow additional token types to be loaded. Token definitions are loaded + * from the provided class loader. + * + */ +public class ExchangeAttributeParser { + + private static final Logger log = Logger.getLogger(ExchangeAttributeParser.class); + + private final List builders; + private final List wrappers; + + public ExchangeAttributeParser(List wrappers) { + this(ExchangeAttributeParser.class.getClassLoader(), wrappers); + } + + public ExchangeAttributeParser(final ClassLoader classLoader, List wrappers) { + this.wrappers = wrappers; + ServiceLoader loader = ServiceLoader.load(ExchangeAttributeBuilder.class, classLoader); + final List builders = new ArrayList<>(); + for (ExchangeAttributeBuilder instance : loader) { + builders.add(instance); + } + //sort with highest priority first + Collections.sort(builders, new Comparator() { + @Override + public int compare(ExchangeAttributeBuilder o1, ExchangeAttributeBuilder o2) { + return Integer.compare(o2.priority(), o1.priority()); + } + }); + this.builders = Collections.unmodifiableList(builders); + + } + + /** + * Parses the provided value string, and turns it into a list of exchange attributes. + *

+ * Tokens are created according to the following rules: + *

+ * %a - % followed by single character. %% is an escape for a literal % + * %{.*}a? - % plus curly braces with any amount of content inside, followed by an optional character + * ${.*} - $ followed by a curly braces to reference an item from the predicate context + * + * @param valueString + * @return + */ + public ExchangeAttribute parse(final String valueString) { + final List attributes = new ArrayList<>(); + int pos = 0; + int state = 0; //0 = literal, 1 = %, 2 = %{, 3 = $, 4 = ${ + for (int i = 0; i < valueString.length(); ++i) { + char c = valueString.charAt(i); + switch (state) { + case 0: { + if (c == '%' || c == '$') { + if (pos != i) { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i)))); + pos = i; + } + if (c == '%') { + state = 1; + } else { + state = 3; + } + } + break; + } + case 1: { + if (c == '{') { + state = 2; + } else if (c == '%') { + //literal percent + attributes.add(wrap(new ConstantExchangeAttribute("%"))); + pos = i + 1; + state = 0; + } else { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1)))); + pos = i + 1; + state = 0; + } + break; + } + case 2: { + if (c == '}') { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1)))); + pos = i + 1; + state = 0; + } + break; + } + case 3: { + if (c == '{') { + state = 4; + } else if (c == '$') { + //literal dollars + attributes.add(wrap(new ConstantExchangeAttribute("$"))); + pos = i + 1; + state = 0; + } else { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1)))); + pos = i + 1; + state = 0; + } + break; + } + case 4: { + if (c == '}') { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1)))); + pos = i + 1; + state = 0; + } + break; + } + + } + } + switch (state) { + case 0: + case 1: + case 3: { + if (pos != valueString.length()) { + attributes.add(wrap(parseSingleToken(valueString.substring(pos)))); + } + break; + } + case 2: + case 4: { + throw new RuntimeException("Mismatched braces: " + valueString); + } + } + if (attributes.size() == 1) { + return attributes.get(0); + } + return new CompositeExchangeAttribute(attributes.toArray(new ExchangeAttribute[attributes.size()])); + } + + public ExchangeAttribute parseSingleToken(final String token) { + for (final ExchangeAttributeBuilder builder : builders) { + ExchangeAttribute res = builder.build(token); + if (res != null) { + return res; + } + } + if (token.startsWith("%")) { + log.errorf("Unknown token %s", token); + } + return new ConstantExchangeAttribute(token); + } + + private ExchangeAttribute wrap(ExchangeAttribute attribute) { + ExchangeAttribute res = attribute; + for (ExchangeAttributeWrapper w : wrappers) { + res = w.wrap(res); + } + return res; + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeWrapper.java new file mode 100644 index 0000000000000..baa1e747cf710 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ExchangeAttributeWrapper.java @@ -0,0 +1,11 @@ +package io.quarkus.vertx.http.runtime.attribute; + +/** + * Interface that can be used to wrap an exchange attribute. + * + */ +public interface ExchangeAttributeWrapper { + + ExchangeAttribute wrap(ExchangeAttribute attribute); + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/IdentUsernameAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/IdentUsernameAttribute.java new file mode 100644 index 0000000000000..f2e61ad7ff212 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/IdentUsernameAttribute.java @@ -0,0 +1,49 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The ident username, not used, included for apache access log compatibility + * + */ +public class IdentUsernameAttribute implements ExchangeAttribute { + + public static final String IDENT_USERNAME = "%l"; + + public static final ExchangeAttribute INSTANCE = new IdentUsernameAttribute(); + + private IdentUsernameAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return null; + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Ident username", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Ident Username"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(IDENT_USERNAME)) { + return IdentUsernameAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalIPAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalIPAttribute.java new file mode 100644 index 0000000000000..15ba84f731f70 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalIPAttribute.java @@ -0,0 +1,51 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.web.RoutingContext; + +/** + * The local IP address + */ +public class LocalIPAttribute implements ExchangeAttribute { + + public static final String LOCAL_IP = "%{LOCAL_IP}"; + public static final String LOCAL_IP_SHORT = "%A"; + + public static final ExchangeAttribute INSTANCE = new LocalIPAttribute(); + + private LocalIPAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + SocketAddress localAddress = exchange.request().localAddress(); + return localAddress.host(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Local IP", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Local IP"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(LOCAL_IP) || token.equals(LOCAL_IP_SHORT)) { + return LocalIPAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalPortAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalPortAttribute.java new file mode 100644 index 0000000000000..93e8da3d9dba3 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalPortAttribute.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The local port + * + */ +public class LocalPortAttribute implements ExchangeAttribute { + + public static final String LOCAL_PORT_SHORT = "%p"; + public static final String LOCAL_PORT = "%{LOCAL_PORT}"; + + public static final ExchangeAttribute INSTANCE = new LocalPortAttribute(); + + private LocalPortAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return Integer.toString(exchange.request().localAddress().port()); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Local port", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Local Port"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(LOCAL_PORT) || token.equals(LOCAL_PORT_SHORT)) { + return LocalPortAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalServerNameAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalServerNameAttribute.java new file mode 100644 index 0000000000000..91d1ef1ade649 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/LocalServerNameAttribute.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The local server name + * + */ +public class LocalServerNameAttribute implements ExchangeAttribute { + + public static final String LOCAL_SERVER_NAME_SHORT = "%v"; + public static final String LOCAL_SERVER_NAME = "%{LOCAL_SERVER_NAME}"; + + public static final ExchangeAttribute INSTANCE = new LocalServerNameAttribute(); + + private LocalServerNameAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return exchange.request().host(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Local server name", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Local server name"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(LOCAL_SERVER_NAME) || token.equals(LOCAL_SERVER_NAME_SHORT)) { + return LocalServerNameAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/NullAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/NullAttribute.java new file mode 100644 index 0000000000000..80f18b0cd31e7 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/NullAttribute.java @@ -0,0 +1,45 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +public class NullAttribute implements ExchangeAttribute { + + public static final String NAME = "%{NULL}"; + + public static final NullAttribute INSTANCE = new NullAttribute(); + + private NullAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return null; + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException(NAME, newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "null"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(NAME)) { + return INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryParameterAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryParameterAttribute.java new file mode 100644 index 0000000000000..9bf0a2dd6c033 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryParameterAttribute.java @@ -0,0 +1,70 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import java.util.ArrayDeque; +import java.util.List; + +import io.vertx.ext.web.RoutingContext; + +/** + * Query parameter + */ +public class QueryParameterAttribute implements ExchangeAttribute { + + private final String parameter; + + public QueryParameterAttribute(String parameter) { + this.parameter = parameter; + } + + @Override + public String readAttribute(final RoutingContext exchange) { + List res = exchange.queryParams().getAll(parameter); + if (res == null) { + return null; + } else if (res.isEmpty()) { + return ""; + } else if (res.size() == 1) { + return res.get(0); + } else { + StringBuilder sb = new StringBuilder("["); + int i = 0; + for (String s : res) { + sb.append(s); + if (++i != res.size()) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + final ArrayDeque value = new ArrayDeque<>(); + value.add(newValue); + exchange.queryParams().set(parameter, value); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Query Parameter"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{q,") && token.endsWith("}")) { + final String qp = token.substring(4, token.length() - 1); + return new QueryParameterAttribute(qp); + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryStringAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryStringAttribute.java new file mode 100644 index 0000000000000..d35d8065a24c6 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QueryStringAttribute.java @@ -0,0 +1,63 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The query string + * + */ +public class QueryStringAttribute implements ExchangeAttribute { + + public static final String QUERY_STRING_SHORT = "%q"; + public static final String QUERY_STRING = "%{QUERY_STRING}"; + public static final String BARE_QUERY_STRING = "%{BARE_QUERY_STRING}"; + + public static final ExchangeAttribute INSTANCE = new QueryStringAttribute(true); + public static final ExchangeAttribute BARE_INSTANCE = new QueryStringAttribute(false); + + private final boolean includeQuestionMark; + + private QueryStringAttribute(boolean includeQuestionMark) { + this.includeQuestionMark = includeQuestionMark; + } + + @Override + public String readAttribute(final RoutingContext exchange) { + String qs = exchange.request().query(); + if (qs == null) { + qs = ""; + } + if (qs.isEmpty() || !includeQuestionMark) { + return qs; + } + return '?' + qs; + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException(); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Query String"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(QUERY_STRING) || token.equals(QUERY_STRING_SHORT)) { + return QueryStringAttribute.INSTANCE; + } else if (token.equals(BARE_QUERY_STRING)) { + return QueryStringAttribute.BARE_INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QuotingExchangeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QuotingExchangeAttribute.java new file mode 100644 index 0000000000000..ef34864617e6c --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/QuotingExchangeAttribute.java @@ -0,0 +1,61 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * Exchange attribute that wraps string attributes in quotes. + * + * This is mostly used + * + */ +public class QuotingExchangeAttribute implements ExchangeAttribute { + + private final ExchangeAttribute exchangeAttribute; + + public static final ExchangeAttributeWrapper WRAPPER = new Wrapper(); + + public QuotingExchangeAttribute(ExchangeAttribute exchangeAttribute) { + this.exchangeAttribute = exchangeAttribute; + } + + @Override + public String readAttribute(RoutingContext exchange) { + String svalue = exchangeAttribute.readAttribute(exchange); + // Does the value contain a " ? If so must encode it + if (svalue == null || "-".equals(svalue) || svalue.isEmpty()) { + return "-"; + } + + /* Wrap all quotes in double quotes. */ + StringBuilder buffer = new StringBuilder(svalue.length() + 2); + buffer.append('\''); + int i = 0; + while (i < svalue.length()) { + int j = svalue.indexOf('\'', i); + if (j == -1) { + buffer.append(svalue.substring(i)); + i = svalue.length(); + } else { + buffer.append(svalue.substring(i, j + 1)); + buffer.append('"'); + i = j + 2; + } + } + + buffer.append('\''); + return buffer.toString(); + } + + @Override + public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException(); + } + + public static class Wrapper implements ExchangeAttributeWrapper { + + @Override + public ExchangeAttribute wrap(final ExchangeAttribute attribute) { + return new QuotingExchangeAttribute(attribute); + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ReadOnlyAttributeException.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ReadOnlyAttributeException.java new file mode 100644 index 0000000000000..f16cc98814595 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ReadOnlyAttributeException.java @@ -0,0 +1,16 @@ +package io.quarkus.vertx.http.runtime.attribute; + +/** + * An exception that is thrown when an attribute is read only + * + */ +public class ReadOnlyAttributeException extends Exception { + + public ReadOnlyAttributeException() { + } + + public ReadOnlyAttributeException(final String attributeName, final String newValue) { + super("Could not set " + attributeName + " to " + newValue); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteHostAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteHostAttribute.java new file mode 100644 index 0000000000000..cd7be81b219ea --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteHostAttribute.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The remote Host address (if resolved) + * + */ +public class RemoteHostAttribute implements ExchangeAttribute { + + public static final String REMOTE_HOST_NAME_SHORT = "%h"; + public static final String REMOTE_HOST = "%{REMOTE_HOST}"; + + public static final ExchangeAttribute INSTANCE = new RemoteHostAttribute(); + + private RemoteHostAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return exchange.request().remoteAddress().host(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Remote host", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Remote host"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REMOTE_HOST) || token.equals(REMOTE_HOST_NAME_SHORT)) { + return RemoteHostAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteIPAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteIPAttribute.java new file mode 100644 index 0000000000000..90bb77f4334a4 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteIPAttribute.java @@ -0,0 +1,52 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.web.RoutingContext; + +/** + * The remote IP address + * + */ +public class RemoteIPAttribute implements ExchangeAttribute { + + public static final String REMOTE_IP_SHORT = "%a"; + public static final String REMOTE_IP = "%{REMOTE_IP}"; + + public static final ExchangeAttribute INSTANCE = new RemoteIPAttribute(); + + private RemoteIPAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + final SocketAddress sourceAddress = exchange.request().remoteAddress(); + return sourceAddress.host(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Remote IP", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Remote IP"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REMOTE_IP) || token.equals(REMOTE_IP_SHORT)) { + return RemoteIPAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteUserAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteUserAttribute.java new file mode 100644 index 0000000000000..44c48bc674655 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RemoteUserAttribute.java @@ -0,0 +1,55 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.vertx.ext.web.RoutingContext; + +/** + * The remote user + * + */ +public class RemoteUserAttribute implements ExchangeAttribute { + + public static final String REMOTE_USER_SHORT = "%u"; + public static final String REMOTE_USER = "%{REMOTE_USER}"; + + public static final ExchangeAttribute INSTANCE = new RemoteUserAttribute(); + + private RemoteUserAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + QuarkusHttpUser sc = (QuarkusHttpUser) exchange.user(); + if (sc == null) { + return null; + } + return sc.getSecurityIdentity().getPrincipal().getName(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Remote user", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Remote user"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REMOTE_USER) || token.equals(REMOTE_USER_SHORT)) { + return RemoteUserAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestHeaderAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestHeaderAttribute.java new file mode 100644 index 0000000000000..12b112c0cc318 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestHeaderAttribute.java @@ -0,0 +1,69 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import java.util.List; + +import io.vertx.ext.web.RoutingContext; + +/** + * A request header + * + */ +public class RequestHeaderAttribute implements ExchangeAttribute { + + private final String requestHeader; + + public RequestHeaderAttribute(final String requestHeader) { + this.requestHeader = requestHeader; + } + + @Override + public String readAttribute(final RoutingContext exchange) { + List header = exchange.request().headers().getAll(requestHeader); + if (header.isEmpty()) { + return null; + } else if (header.size() == 1) { + return header.get(0); + } + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int i = 0; i < header.size(); ++i) { + if (i != 0) { + sb.append(", "); + } + sb.append(header.get(i)); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + if (newValue == null) { + exchange.request().headers().remove(requestHeader); + } else { + exchange.request().headers().set(requestHeader, newValue); + } + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request header"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{i,") && token.endsWith("}")) { + final String headerName = token.substring(4, token.length() - 1); + return new RequestHeaderAttribute(headerName); + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestLineAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestLineAttribute.java new file mode 100644 index 0000000000000..b53ea4ad5cf67 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestLineAttribute.java @@ -0,0 +1,60 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The request line + * + */ +public class RequestLineAttribute implements ExchangeAttribute { + + public static final String REQUEST_LINE_SHORT = "%r"; + public static final String REQUEST_LINE = "%{REQUEST_LINE}"; + + public static final ExchangeAttribute INSTANCE = new RequestLineAttribute(); + + private RequestLineAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + StringBuilder sb = new StringBuilder() + .append(exchange.request().method()) + .append(' ') + .append(exchange.request().uri()); + if (exchange.request().query() != null) { + sb.append('?'); + sb.append(exchange.request().query()); + } + sb.append(' ') + .append(exchange.request().version()); + return sb.toString(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Request line", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request line"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_LINE) || token.equals(REQUEST_LINE_SHORT)) { + return RequestLineAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestMethodAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestMethodAttribute.java new file mode 100644 index 0000000000000..d70abe2ba3f3e --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestMethodAttribute.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The request method + * + */ +public class RequestMethodAttribute implements ExchangeAttribute { + + public static final String REQUEST_METHOD_SHORT = "%m"; + public static final String REQUEST_METHOD = "%{METHOD}"; + + public static final ExchangeAttribute INSTANCE = new RequestMethodAttribute(); + + private RequestMethodAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return exchange.request().method().name(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Request method", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request method"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_METHOD) || token.equals(REQUEST_METHOD_SHORT)) { + return RequestMethodAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestPathAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestPathAttribute.java new file mode 100644 index 0000000000000..fae5f581d20e0 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestPathAttribute.java @@ -0,0 +1,42 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +public class RequestPathAttribute implements ExchangeAttribute { + + public static final String REQUEST_PATH = "%{REQUEST_PATH}"; + + public static final ExchangeAttribute INSTANCE = new RequestPathAttribute(); + + private RequestPathAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return exchange.request().path(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException(); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request Path"; + } + + @Override + public ExchangeAttribute build(final String token) { + return token.equals(REQUEST_PATH) ? INSTANCE : null; + } + + @Override + public int priority() { + return 0; + } + } +} \ No newline at end of file diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestProtocolAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestProtocolAttribute.java new file mode 100644 index 0000000000000..e1e08e19241b9 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestProtocolAttribute.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The request getProtocol + * + */ +public class RequestProtocolAttribute implements ExchangeAttribute { + + public static final String REQUEST_PROTOCOL_SHORT = "%H"; + public static final String REQUEST_PROTOCOL = "%{PROTOCOL}"; + + public static final ExchangeAttribute INSTANCE = new RequestProtocolAttribute(); + + private RequestProtocolAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return exchange.request().version().name(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Request getProtocol", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request getProtocol"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_PROTOCOL) || token.equals(REQUEST_PROTOCOL_SHORT)) { + return RequestProtocolAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestSchemeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestSchemeAttribute.java new file mode 100644 index 0000000000000..df1715432133b --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestSchemeAttribute.java @@ -0,0 +1,49 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The request scheme + * + */ +public class RequestSchemeAttribute implements ExchangeAttribute { + + public static final String REQUEST_SCHEME = "%{SCHEME}"; + + public static final ExchangeAttribute INSTANCE = new RequestSchemeAttribute(); + + private RequestSchemeAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return exchange.request().scheme(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException(); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request scheme"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_SCHEME)) { + return RequestSchemeAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestURLAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestURLAttribute.java new file mode 100644 index 0000000000000..046d869ee13f2 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/RequestURLAttribute.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The request URL + * + */ +public class RequestURLAttribute implements ExchangeAttribute { + + public static final String REQUEST_URL_SHORT = "%U"; + public static final String REQUEST_URL = "%{REQUEST_URL}"; + + public static final ExchangeAttribute INSTANCE = new RequestURLAttribute(); + + private RequestURLAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return exchange.request().uri(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException(); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request URL"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_URL) || token.equals(REQUEST_URL_SHORT)) { + return RequestURLAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseCodeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseCodeAttribute.java new file mode 100644 index 0000000000000..dfcd8455fd155 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseCodeAttribute.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The request status code + * + */ +public class ResponseCodeAttribute implements ExchangeAttribute { + + public static final String RESPONSE_CODE_SHORT = "%s"; + public static final String RESPONSE_CODE = "%{RESPONSE_CODE}"; + + public static final ExchangeAttribute INSTANCE = new ResponseCodeAttribute(); + + private ResponseCodeAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return Integer.toString(exchange.response().getStatusCode()); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + exchange.response().setStatusCode(Integer.parseInt(newValue)); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Response code"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(RESPONSE_CODE) || token.equals(RESPONSE_CODE_SHORT)) { + return ResponseCodeAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseHeaderAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseHeaderAttribute.java new file mode 100644 index 0000000000000..78621d6c783ef --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseHeaderAttribute.java @@ -0,0 +1,69 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import java.util.List; + +import io.vertx.ext.web.RoutingContext; + +/** + * A response header + * + */ +public class ResponseHeaderAttribute implements ExchangeAttribute { + + private final String responseHeader; + + public ResponseHeaderAttribute(final String responseHeader) { + this.responseHeader = responseHeader; + } + + @Override + public String readAttribute(final RoutingContext exchange) { + List header = exchange.response().headers().getAll(responseHeader); + if (header.isEmpty()) { + return null; + } else if (header.size() == 1) { + return header.get(0); + } + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int i = 0; i < header.size(); ++i) { + if (i != 0) { + sb.append(", "); + } + sb.append(header.get(i)); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + if (newValue == null) { + exchange.response().headers().remove(responseHeader); + } else { + exchange.response().headers().set(responseHeader, newValue); + } + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Response header"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{o,") && token.endsWith("}")) { + final String headerName = token.substring(4, token.length() - 1); + return new ResponseHeaderAttribute(headerName); + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseTimeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseTimeAttribute.java new file mode 100644 index 0000000000000..d7a550e1f6b66 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ResponseTimeAttribute.java @@ -0,0 +1,97 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import java.util.concurrent.TimeUnit; + +import io.quarkus.vertx.http.runtime.VertxHttpRecorder; +import io.vertx.ext.web.RoutingContext; + +/** + * The response time + * + * This will only work if {@link io.quarkus.vertx.http.runtime.HttpConfiguration#recordRequestStartTime} has been set + */ +public class ResponseTimeAttribute implements ExchangeAttribute { + + private static final String FIRST_RESPONSE_TIME_NANOS = ResponseTimeAttribute.class.getName() + ".first-response-time"; + + public static final String RESPONSE_TIME_MILLIS_SHORT = "%D"; + public static final String RESPONSE_TIME_SECONDS_SHORT = "%T"; + public static final String RESPONSE_TIME_MILLIS = "%{RESPONSE_TIME}"; + public static final String RESPONSE_TIME_MICROS = "%{RESPONSE_TIME_MICROS}"; + public static final String RESPONSE_TIME_NANOS = "%{RESPONSE_TIME_NANOS}"; + + private final TimeUnit timeUnit; + + public ResponseTimeAttribute(TimeUnit timeUnit) { + this.timeUnit = timeUnit; + } + + @Override + public String readAttribute(RoutingContext exchange) { + Long requestStartTime = exchange.get(VertxHttpRecorder.REQUEST_START_TIME); + if (requestStartTime == null) { + return null; + } + final long nanos; + Long first = exchange.get(FIRST_RESPONSE_TIME_NANOS); + if (first != null) { + nanos = first; + } else { + nanos = System.nanoTime() - requestStartTime; + if (exchange.response().ended()) { + //save the response time so it is consistent + exchange.put(FIRST_RESPONSE_TIME_NANOS, nanos); + } + } + if (timeUnit == TimeUnit.SECONDS) { + StringBuilder buf = new StringBuilder(); + long milis = TimeUnit.MILLISECONDS.convert(nanos, TimeUnit.NANOSECONDS); + buf.append(Long.toString(milis / 1000)); + buf.append('.'); + int remains = (int) (milis % 1000); + buf.append(Long.toString(remains / 100)); + remains = remains % 100; + buf.append(Long.toString(remains / 10)); + buf.append(Long.toString(remains % 10)); + return buf.toString(); + } else { + return String.valueOf(timeUnit.convert(nanos, TimeUnit.NANOSECONDS)); + } + } + + @Override + public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Response Time", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Response Time"; + } + + @Override + public ExchangeAttribute build(String token) { + if (token.equals(RESPONSE_TIME_MILLIS) || token.equals(RESPONSE_TIME_MILLIS_SHORT)) { + return new ResponseTimeAttribute(TimeUnit.MILLISECONDS); + } + if (token.equals(RESPONSE_TIME_SECONDS_SHORT)) { + return new ResponseTimeAttribute(TimeUnit.SECONDS); + } + if (token.equals(RESPONSE_TIME_MICROS)) { + return new ResponseTimeAttribute(TimeUnit.MICROSECONDS); + } + if (token.equals(RESPONSE_TIME_NANOS)) { + return new ResponseTimeAttribute(TimeUnit.NANOSECONDS); + } + return null; + } + + @Override + public int priority() { + return 0; + } + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SecureExchangeAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SecureExchangeAttribute.java new file mode 100644 index 0000000000000..45c44f137b7ff --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SecureExchangeAttribute.java @@ -0,0 +1,42 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +public class SecureExchangeAttribute implements ExchangeAttribute { + + public static final String TOKEN = "%{SECURE}"; + + public static final String LEGACY_INCORRECT_TOKEN = "${SECURE}"; //this was a bug, but we still support it for compat + public static final ExchangeAttribute INSTANCE = new SecureExchangeAttribute(); + + @Override + public String readAttribute(RoutingContext exchange) { + return Boolean.toString(exchange.request().scheme().equals("https")); + } + + @Override + public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException(); + } + + public static class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Secure"; + } + + @Override + public ExchangeAttribute build(String token) { + if (token.equals(TOKEN) || token.equals(LEGACY_INCORRECT_TOKEN)) { + return INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslCipherAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslCipherAttribute.java new file mode 100644 index 0000000000000..bebad9687299b --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslCipherAttribute.java @@ -0,0 +1,45 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import javax.net.ssl.SSLSession; + +import io.vertx.ext.web.RoutingContext; + +public class SslCipherAttribute implements ExchangeAttribute { + + public static final SslCipherAttribute INSTANCE = new SslCipherAttribute(); + + @Override + public String readAttribute(RoutingContext exchange) { + SSLSession ssl = exchange.request().sslSession(); + if (ssl == null) { + return null; + } + return ssl.getCipherSuite(); + } + + @Override + public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("SSL Cipher", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "SSL Cipher"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals("%{SSL_CIPHER}")) { + return INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslClientCertAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslClientCertAttribute.java new file mode 100644 index 0000000000000..8ee7a16f71684 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslClientCertAttribute.java @@ -0,0 +1,74 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import java.util.Base64; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.security.cert.CertificateEncodingException; +import javax.security.cert.X509Certificate; + +import io.vertx.ext.web.RoutingContext; + +public class SslClientCertAttribute implements ExchangeAttribute { + + public static final SslClientCertAttribute INSTANCE = new SslClientCertAttribute(); + public static final java.lang.String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + + public static final java.lang.String END_CERT = "-----END CERTIFICATE-----"; + + public static String toPem(final X509Certificate certificate) throws CertificateEncodingException { + final StringBuilder builder = new StringBuilder(); + builder.append(BEGIN_CERT); + builder.append('\n'); + builder.append(Base64.getEncoder().encodeToString(certificate.getEncoded())); + builder.append('\n'); + builder.append(END_CERT); + return builder.toString(); + } + + @Override + public String readAttribute(RoutingContext exchange) { + SSLSession ssl = exchange.request().sslSession(); + if (ssl == null) { + return null; + } + X509Certificate[] certificates; + try { + certificates = ssl.getPeerCertificateChain(); + if (certificates.length > 0) { + return toPem(certificates[0]); + } + return null; + } catch (SSLPeerUnverifiedException e) { + return null; + } catch (CertificateEncodingException e) { + return null; + } + } + + @Override + public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("SSL Client Cert", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "SSL Client Cert"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals("%{SSL_CLIENT_CERT}")) { + return INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslSessionIdAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslSessionIdAttribute.java new file mode 100644 index 0000000000000..cd89f479a0f2c --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SslSessionIdAttribute.java @@ -0,0 +1,47 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import java.util.Base64; + +import javax.net.ssl.SSLSession; + +import io.vertx.ext.web.RoutingContext; + +public class SslSessionIdAttribute implements ExchangeAttribute { + + public static final SslSessionIdAttribute INSTANCE = new SslSessionIdAttribute(); + + @Override + public String readAttribute(RoutingContext exchange) { + SSLSession ssl = exchange.request().sslSession(); + if (ssl == null || ssl.getId() == null) { + return null; + } + return Base64.getEncoder().encodeToString(ssl.getId()); + } + + @Override + public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("SSL Session ID", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "SSL Session ID"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals("%{SSL_SESSION_ID}")) { + return INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SubstituteEmptyWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SubstituteEmptyWrapper.java new file mode 100644 index 0000000000000..868571b4bd9d9 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/SubstituteEmptyWrapper.java @@ -0,0 +1,41 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +public class SubstituteEmptyWrapper implements ExchangeAttributeWrapper { + + private final String substitute; + + public SubstituteEmptyWrapper(String substitute) { + this.substitute = substitute; + } + + @Override + public ExchangeAttribute wrap(final ExchangeAttribute attribute) { + return new SubstituteEmptyAttribute(attribute, substitute); + } + + public static class SubstituteEmptyAttribute implements ExchangeAttribute { + private final ExchangeAttribute attribute; + private final String substitute; + + public SubstituteEmptyAttribute(ExchangeAttribute attribute, String substitute) { + this.attribute = attribute; + this.substitute = substitute; + } + + @Override + public String readAttribute(RoutingContext exchange) { + String val = attribute.readAttribute(exchange); + if (val == null || val.isEmpty()) { + return substitute; + } + return val; + } + + @Override + public void writeAttribute(RoutingContext exchange, String newValue) throws ReadOnlyAttributeException { + attribute.writeAttribute(exchange, newValue); + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ThreadNameAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ThreadNameAttribute.java new file mode 100644 index 0000000000000..d8a5a79ded87d --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/ThreadNameAttribute.java @@ -0,0 +1,50 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +/** + * The thread name + * + */ +public class ThreadNameAttribute implements ExchangeAttribute { + + public static final String THREAD_NAME_SHORT = "%I"; + public static final String THREAD_NAME = "%{THREAD_NAME}"; + + public static final ExchangeAttribute INSTANCE = new ThreadNameAttribute(); + + private ThreadNameAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return Thread.currentThread().getName(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Thread name", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Thread name"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(THREAD_NAME) || token.equals(THREAD_NAME_SHORT)) { + return ThreadNameAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/TransportProtocolAttribute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/TransportProtocolAttribute.java new file mode 100644 index 0000000000000..0133cd27f27f1 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/attribute/TransportProtocolAttribute.java @@ -0,0 +1,45 @@ +package io.quarkus.vertx.http.runtime.attribute; + +import io.vertx.ext.web.RoutingContext; + +public class TransportProtocolAttribute implements ExchangeAttribute { + + public static final String TRANSPORT_PROTOCOL = "%{TRANSPORT_PROTOCOL}"; + + public static final ExchangeAttribute INSTANCE = new TransportProtocolAttribute(); + + private TransportProtocolAttribute() { + + } + + @Override + public String readAttribute(final RoutingContext exchange) { + return exchange.request().scheme(); + } + + @Override + public void writeAttribute(final RoutingContext exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("transport getProtocol", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Transport Protocol"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(TRANSPORT_PROTOCOL)) { + return TransportProtocolAttribute.INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/GracefulShutdownFilter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/GracefulShutdownFilter.java index 4cc5bc79d5825..de9cee1719610 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/GracefulShutdownFilter.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/GracefulShutdownFilter.java @@ -45,7 +45,8 @@ public void handle(HttpServerRequest event) { } currentRequestCount.incrementAndGet(); //todo: some way to do this without a wrapper solution - next.handle(new ShutdownRequestWrapper(event, requestDoneHandler)); + ((QuarkusRequestWrapper) event).addRequestDoneHandler(requestDoneHandler); + next.handle(event); } @Override diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/QuarkusRequestWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/QuarkusRequestWrapper.java new file mode 100644 index 0000000000000..d2a054be07a0f --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/QuarkusRequestWrapper.java @@ -0,0 +1,209 @@ +package io.quarkus.vertx.http.runtime.filters; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import org.jboss.logging.Logger; + +import io.quarkus.vertx.http.runtime.AbstractRequestWrapper; +import io.vertx.core.Handler; +import io.vertx.core.http.Cookie; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; + +public class QuarkusRequestWrapper extends AbstractRequestWrapper { + + /** + * Huge hack, to work around the fact that there is no way to directly access this class once it is wrapped, + * we use a fake cookie + */ + public static final String FAKE_COOKIE_NAME = "X-quarkus-request-wrapper"; + + private static final Logger log = Logger.getLogger(QuarkusRequestWrapper.class); + + private volatile int done; + private static final AtomicIntegerFieldUpdater doneUpdater = AtomicIntegerFieldUpdater + .newUpdater(QuarkusRequestWrapper.class, "done"); + + private Handler exceptionHandler; + private final AbstractResponseWrapper response; + + private final List> requestDoneHandlers = new ArrayList<>(); + + public QuarkusRequestWrapper(HttpServerRequest event) { + super(event); + this.response = new ResponseWrapper(delegate.response()); + event.exceptionHandler(new Handler() { + @Override + public void handle(Throwable event) { + if (exceptionHandler != null) { + exceptionHandler.handle(event); + } + done(); + } + }); + } + + public static QuarkusRequestWrapper get(HttpServerRequest request) { + return ((QuarkusRequestWrapper.QuarkusCookie) request.getCookie(QuarkusRequestWrapper.FAKE_COOKIE_NAME)) + .getRequestWrapper(); + } + + public void addRequestDoneHandler(Handler handler) { + this.requestDoneHandlers.add(handler); + } + + @Override + public HttpServerResponse response() { + return response; + } + + void done() { + if (doneUpdater.compareAndSet(this, 0, 1)) { + for (Handler i : requestDoneHandlers) { + try { + i.handle(null); + } catch (Throwable t) { + log.error("Failed to run " + i, t); + } + } + } + } + + @Override + public HttpServerRequest exceptionHandler(Handler handler) { + exceptionHandler = handler; + return this; + } + + @Override + public Cookie getCookie(String name) { + if (name.equals(FAKE_COOKIE_NAME)) { + return new QuarkusCookie(); + } + return super.getCookie(name); + } + + class ResponseWrapper extends AbstractResponseWrapper { + + Handler endHandler; + Handler closeHandler; + Handler exceptionHandler; + + ResponseWrapper(HttpServerResponse delegate) { + super(delegate); + delegate.closeHandler(new Handler() { + @Override + public void handle(Void event) { + done(); + if (closeHandler != null) { + closeHandler.handle(event); + } + } + }); + delegate.exceptionHandler(new Handler() { + @Override + public void handle(Throwable event) { + done(); + + if (exceptionHandler != null) { + exceptionHandler.handle(event); + } + } + }); + delegate.endHandler(new Handler() { + @Override + public void handle(Void event) { + done(); + if (endHandler != null) { + endHandler.handle(event); + } + } + }); + } + + @Override + public HttpServerResponse exceptionHandler(Handler handler) { + this.exceptionHandler = handler; + return this; + } + + @Override + public HttpServerResponse closeHandler(Handler handler) { + this.closeHandler = handler; + return this; + } + + @Override + public HttpServerResponse endHandler(Handler handler) { + this.endHandler = handler; + return this; + } + } + + public class QuarkusCookie implements Cookie { + + public QuarkusRequestWrapper getRequestWrapper() { + //it does not get much more hacky than this + //hopefully we can work around it in the next Vert.x version + return QuarkusRequestWrapper.this; + } + + @Override + public String getName() { + return null; + } + + @Override + public String getValue() { + return null; + } + + @Override + public Cookie setValue(String value) { + return null; + } + + @Override + public Cookie setDomain(String domain) { + return null; + } + + @Override + public String getDomain() { + return null; + } + + @Override + public Cookie setPath(String path) { + return null; + } + + @Override + public String getPath() { + return null; + } + + @Override + public Cookie setMaxAge(long maxAge) { + return null; + } + + @Override + public Cookie setSecure(boolean secure) { + return null; + } + + @Override + public Cookie setHttpOnly(boolean httpOnly) { + return null; + } + + @Override + public String encode() { + return null; + } + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/ShutdownRequestWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/ShutdownRequestWrapper.java deleted file mode 100644 index c576a98c5c1d8..0000000000000 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/ShutdownRequestWrapper.java +++ /dev/null @@ -1,109 +0,0 @@ -package io.quarkus.vertx.http.runtime.filters; - -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - -import io.quarkus.vertx.http.runtime.AbstractRequestWrapper; -import io.vertx.core.Handler; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; - -class ShutdownRequestWrapper extends AbstractRequestWrapper { - - private volatile int done; - private static final AtomicIntegerFieldUpdater doneUpdater = AtomicIntegerFieldUpdater - .newUpdater(ShutdownRequestWrapper.class, "done"); - private final Handler requestDoneHandler; - - private Handler exceptionHandler; - private final AbstractResponseWrapper response; - - public ShutdownRequestWrapper(HttpServerRequest event, Handler requestDoneHandler) { - super(event); - this.requestDoneHandler = requestDoneHandler; - this.response = new ResponseWrapper(delegate.response()); - event.exceptionHandler(new Handler() { - @Override - public void handle(Throwable event) { - if (exceptionHandler != null) { - exceptionHandler.handle(event); - } - done(); - } - }); - } - - @Override - public HttpServerResponse response() { - return response; - } - - void done() { - if (doneUpdater.compareAndSet(this, 0, 1)) { - requestDoneHandler.handle(null); - } - } - - @Override - public HttpServerRequest exceptionHandler(Handler handler) { - exceptionHandler = handler; - return this; - } - - class ResponseWrapper extends AbstractResponseWrapper { - - Handler endHandler; - Handler closeHandler; - Handler exceptionHandler; - - ResponseWrapper(HttpServerResponse delegate) { - super(delegate); - delegate.closeHandler(new Handler() { - @Override - public void handle(Void event) { - done(); - if (closeHandler != null) { - closeHandler.handle(event); - } - } - }); - delegate.exceptionHandler(new Handler() { - @Override - public void handle(Throwable event) { - done(); - - if (exceptionHandler != null) { - exceptionHandler.handle(event); - } - } - }); - delegate.endHandler(new Handler() { - @Override - public void handle(Void event) { - done(); - if (endHandler != null) { - endHandler.handle(event); - } - } - }); - } - - @Override - public HttpServerResponse exceptionHandler(Handler handler) { - this.exceptionHandler = handler; - return this; - } - - @Override - public HttpServerResponse closeHandler(Handler handler) { - this.closeHandler = handler; - return this; - } - - @Override - public HttpServerResponse endHandler(Handler handler) { - this.endHandler = handler; - return this; - } - } - -} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogHandler.java new file mode 100644 index 0000000000000..252f8554a2dc4 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogHandler.java @@ -0,0 +1,133 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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.quarkus.vertx.http.runtime.filters.accesslog; + +import java.util.Collections; + +import io.quarkus.vertx.http.runtime.attribute.ExchangeAttribute; +import io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeParser; +import io.quarkus.vertx.http.runtime.attribute.SubstituteEmptyWrapper; +import io.quarkus.vertx.http.runtime.filters.QuarkusRequestWrapper; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Access log handler. This handler will generate access log messages based on the provided format string, + * and pass these messages into the provided {@link AccessLogReceiver}. + *

+ * This handler can log any attribute that is provides via the {@link ExchangeAttribute} + * mechanism. A general guide to the most common attribute is provided before, however this mechanism is extensible. + *

+ *

+ *

+ * This factory produces token handlers for the following patterns + *

+ *
    + *
  • %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 getProtocol + *
  • %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, in Common Log Format format + *
  • %u - Remote user that was authenticated + *
  • %U - Requested URL path + *
  • %v - Local server name + *
  • %D - Time taken to process the request, in millis + *
  • %T - Time taken to process the request, in seconds + *
  • %I - current Request thread name (can compare later with stacktraces) + *
+ *

+ * In addition, the caller can specify one of the following aliases for + * commonly utilized patterns: + *

+ *
    + *
  • common - %h %l %u %t "%r" %s %b + *
  • combined - + * %h %l %u %t "%r" %s %b "%{i,Referer}" "%{i,User-Agent}" + *
+ *

+ *

+ * There is also support to write information from the cookie, incoming + * header, or the session
+ * It is modeled after the apache syntax: + *

    + *
  • %{i,xxx} for incoming headers + *
  • %{o,xxx} for outgoing response headers + *
  • %{c,xxx} for a specific cookie + *
  • %{r,xxx} xxx is an attribute in the ServletRequest + *
  • %{s,xxx} xxx is an attribute in the HttpSession + *
+ * + * @author Stuart Douglas + */ +public class AccessLogHandler implements Handler { + + private final AccessLogReceiver accessLogReceiver; + private final String formatString; + private final ExchangeAttribute tokens; + + public AccessLogHandler(final AccessLogReceiver accessLogReceiver, final String formatString, ClassLoader classLoader) { + this.accessLogReceiver = accessLogReceiver; + this.formatString = handleCommonNames(formatString); + this.tokens = new ExchangeAttributeParser(classLoader, Collections.singletonList(new SubstituteEmptyWrapper("-"))) + .parse(this.formatString); + } + + public AccessLogHandler(final AccessLogReceiver accessLogReceiver, String formatString, final ExchangeAttribute attribute) { + this.accessLogReceiver = accessLogReceiver; + this.formatString = handleCommonNames(formatString); + this.tokens = attribute; + } + + private static String handleCommonNames(String formatString) { + if (formatString.equals("common")) { + return "%h %l %u %t \"%r\" %s %b"; + } else if (formatString.equals("combined")) { + return "%h %l %u %t \"%r\" %s %b \"%{i,Referer}\" \"%{i,User-Agent}\""; + } + return formatString; + } + + @Override + public void handle(RoutingContext rc) { + QuarkusRequestWrapper.get(rc.request()).addRequestDoneHandler(new Handler() { + @Override + public void handle(Void event) { + accessLogReceiver.logMessage(tokens.readAttribute(rc)); + } + }); + rc.next(); + } + + @Override + public String toString() { + return "AccessLogHandler{" + + "formatString='" + formatString + '\'' + + '}'; + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogReceiver.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogReceiver.java new file mode 100644 index 0000000000000..ef3750d1c18c9 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/AccessLogReceiver.java @@ -0,0 +1,32 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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.quarkus.vertx.http.runtime.filters.accesslog; + +/** + * Interface that is used by the access log handler to send data to the log file manager. + * + * Implementations of this interface must be thread safe. + * + * @author Stuart Douglas + */ +public interface AccessLogReceiver { + + void logMessage(final String message); + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/DefaultAccessLogReceiver.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/DefaultAccessLogReceiver.java new file mode 100644 index 0000000000000..0985ab9cb3adf --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/DefaultAccessLogReceiver.java @@ -0,0 +1,382 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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.quarkus.vertx.http.runtime.filters.accesslog; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Deque; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import org.jboss.logging.Logger; + +/** + * Log Receiver that stores logs in a directory under the specified file name, and rotates them after + * midnight. + *

+ * Web threads do not touch the log file, but simply queue messages to be written later by a worker thread. + * A lightweight CAS based locking mechanism is used to ensure than only 1 thread is active writing messages at + * any given time + * + * @author Stuart Douglas + */ +public class DefaultAccessLogReceiver implements AccessLogReceiver, Runnable, Closeable { + + private static final Logger log = Logger.getLogger(DefaultAccessLogReceiver.class); + + private static final String DEFAULT_LOG_SUFFIX = "log"; + + private final Executor logWriteExecutor; + + private final Deque pendingMessages; + + //0 = not running + //1 = queued + //2 = running + @SuppressWarnings("unused") + private volatile int state = 0; + + private static final AtomicIntegerFieldUpdater stateUpdater = AtomicIntegerFieldUpdater + .newUpdater(DefaultAccessLogReceiver.class, "state"); + + private long changeOverPoint; + private String currentDateString; + private boolean forceLogRotation; + + private final Path outputDirectory; + private final Path defaultLogFile; + + private final String logBaseName; + private final String logNameSuffix; + + private Writer writer = null; + + private volatile boolean closed = false; + private boolean initialRun = true; + private final boolean rotate; + private final LogFileHeaderGenerator fileHeaderGenerator; + + public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName) { + this(logWriteExecutor, outputDirectory.toPath(), logBaseName, null); + } + + public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName, + final String logNameSuffix) { + this(logWriteExecutor, outputDirectory.toPath(), logBaseName, logNameSuffix, true); + } + + public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName, + final String logNameSuffix, boolean rotate) { + this(logWriteExecutor, outputDirectory.toPath(), logBaseName, logNameSuffix, rotate); + } + + public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName) { + this(logWriteExecutor, outputDirectory, logBaseName, null); + } + + public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, + final String logNameSuffix) { + this(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, true); + } + + public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, + final String logNameSuffix, boolean rotate) { + this(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, rotate, null); + } + + private DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, + final String logNameSuffix, boolean rotate, LogFileHeaderGenerator fileHeader) { + this.logWriteExecutor = logWriteExecutor; + this.outputDirectory = outputDirectory; + this.logBaseName = logBaseName; + this.rotate = rotate; + this.fileHeaderGenerator = fileHeader; + this.logNameSuffix = (logNameSuffix != null) ? logNameSuffix : DEFAULT_LOG_SUFFIX; + this.pendingMessages = new ConcurrentLinkedDeque<>(); + this.defaultLogFile = outputDirectory.resolve(logBaseName + this.logNameSuffix); + calculateChangeOverPoint(); + } + + private void calculateChangeOverPoint() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.add(Calendar.DATE, 1); + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + currentDateString = df.format(new Date()); + // if there is an existing default log file, use the date last modified instead of the current date + if (Files.exists(defaultLogFile)) { + try { + currentDateString = df.format(new Date(Files.getLastModifiedTime(defaultLogFile).toMillis())); + } catch (IOException e) { + // ignore. use the current date if exception happens. + } + } + changeOverPoint = calendar.getTimeInMillis(); + } + + @Override + public void logMessage(final String message) { + this.pendingMessages.add(message); + int state = stateUpdater.get(this); + if (state == 0) { + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } + } + + /** + * processes all queued log messages + */ + @Override + public void run() { + if (!stateUpdater.compareAndSet(this, 1, 2)) { + return; + } + if (forceLogRotation) { + doRotate(); + } else if (initialRun && Files.exists(defaultLogFile)) { + //if there is an existing log file check if it should be rotated + long lm = 0; + try { + lm = Files.getLastModifiedTime(defaultLogFile).toMillis(); + } catch (IOException e) { + log.error("Error rotating access log", e); + } + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(changeOverPoint); + c.add(Calendar.DATE, -1); + if (lm <= c.getTimeInMillis()) { + doRotate(); + } + } + initialRun = false; + List messages = new ArrayList<>(); + String msg; + //only grab at most 1000 messages at a time + for (int i = 0; i < 1000; ++i) { + msg = pendingMessages.poll(); + if (msg == null) { + break; + } + messages.add(msg); + } + try { + if (!messages.isEmpty()) { + writeMessage(messages); + } + } finally { + stateUpdater.set(this, 0); + //check to see if there is still more messages + //if so then run this again + if (!pendingMessages.isEmpty() || forceLogRotation) { + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } else if (closed) { + try { + if (writer != null) { + writer.flush(); + writer.close(); + writer = null; + } + } catch (IOException e) { + log.error("Error writing access log", e); + } + } + } + } + + /** + * For tests only. Blocks the current thread until all messages are written + * Just does a busy wait. + *

+ * DO NOT USE THIS OUTSIDE OF A TEST + */ + void awaitWrittenForTest() throws InterruptedException { + while (!pendingMessages.isEmpty() || forceLogRotation) { + Thread.sleep(10); + } + while (state != 0) { + Thread.sleep(10); + } + } + + private void writeMessage(final List messages) { + if (System.currentTimeMillis() > changeOverPoint) { + doRotate(); + } + try { + if (writer == null) { + boolean created = !Files.exists(defaultLogFile); + writer = Files.newBufferedWriter(defaultLogFile, StandardCharsets.UTF_8, StandardOpenOption.APPEND, + StandardOpenOption.CREATE); + if (Files.size(defaultLogFile) == 0 && fileHeaderGenerator != null) { + String header = fileHeaderGenerator.generateHeader(); + if (header != null) { + writer.write(header); + writer.write("\n"); + writer.flush(); + } + } + } + for (String message : messages) { + writer.write(message); + writer.write('\n'); + } + writer.flush(); + } catch (IOException e) { + log.error("Error writing access log", e); + } + } + + private void doRotate() { + forceLogRotation = false; + if (!rotate) { + return; + } + try { + if (writer != null) { + writer.flush(); + writer.close(); + writer = null; + } + if (!Files.exists(defaultLogFile)) { + return; + } + Path newFile = outputDirectory.resolve(logBaseName + currentDateString + "." + logNameSuffix); + int count = 0; + while (Files.exists(newFile)) { + ++count; + newFile = outputDirectory.resolve(logBaseName + currentDateString + "-" + count + "." + logNameSuffix); + } + Files.move(defaultLogFile, newFile); + } catch (IOException e) { + log.error("Error rotating access log", e); + } finally { + calculateChangeOverPoint(); + } + } + + /** + * forces a log rotation. This rotation is performed in an async manner, you cannot rely on the rotation + * being performed immediately after this method returns. + */ + public void rotate() { + forceLogRotation = true; + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } + + @Override + public void close() throws IOException { + closed = true; + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Executor logWriteExecutor; + private Path outputDirectory; + private String logBaseName; + private String logNameSuffix; + private boolean rotate; + private LogFileHeaderGenerator logFileHeaderGenerator; + + public Executor getLogWriteExecutor() { + return logWriteExecutor; + } + + public Builder setLogWriteExecutor(Executor logWriteExecutor) { + this.logWriteExecutor = logWriteExecutor; + return this; + } + + public Path getOutputDirectory() { + return outputDirectory; + } + + public Builder setOutputDirectory(Path outputDirectory) { + this.outputDirectory = outputDirectory; + return this; + } + + public String getLogBaseName() { + return logBaseName; + } + + public Builder setLogBaseName(String logBaseName) { + this.logBaseName = logBaseName; + return this; + } + + public String getLogNameSuffix() { + return logNameSuffix; + } + + public Builder setLogNameSuffix(String logNameSuffix) { + this.logNameSuffix = logNameSuffix; + return this; + } + + public boolean isRotate() { + return rotate; + } + + public Builder setRotate(boolean rotate) { + this.rotate = rotate; + return this; + } + + public LogFileHeaderGenerator getLogFileHeaderGenerator() { + return logFileHeaderGenerator; + } + + public Builder setLogFileHeaderGenerator(LogFileHeaderGenerator logFileHeaderGenerator) { + this.logFileHeaderGenerator = logFileHeaderGenerator; + return this; + } + + public DefaultAccessLogReceiver build() { + return new DefaultAccessLogReceiver(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, rotate, + logFileHeaderGenerator); + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/JBossLoggingAccessLogReceiver.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/JBossLoggingAccessLogReceiver.java new file mode 100644 index 0000000000000..d9dcdfa05558a --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/JBossLoggingAccessLogReceiver.java @@ -0,0 +1,46 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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.quarkus.vertx.http.runtime.filters.accesslog; + +import org.jboss.logging.Logger; + +/** + * Access log receiver that logs messages at INFO level. + * + * @author Stuart Douglas + */ +public class JBossLoggingAccessLogReceiver implements AccessLogReceiver { + + public static final String DEFAULT_CATEGORY = "io.quarkus.vertx.http.accesslog"; + + private final Logger logger; + + public JBossLoggingAccessLogReceiver(final String category) { + this.logger = Logger.getLogger(category); + } + + public JBossLoggingAccessLogReceiver() { + this.logger = Logger.getLogger(DEFAULT_CATEGORY); + } + + @Override + public void logMessage(String message) { + logger.info(message); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/LogFileHeaderGenerator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/LogFileHeaderGenerator.java new file mode 100644 index 0000000000000..e2c71b8e52718 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/accesslog/LogFileHeaderGenerator.java @@ -0,0 +1,31 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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.quarkus.vertx.http.runtime.filters.accesslog; + +/** + * Interface that generates the header for an access log. This is called + * every time a new log file is started. + * + * @author Stuart Douglas + */ +public interface LogFileHeaderGenerator { + + String generateHeader(); + +} diff --git a/extensions/vertx-http/runtime/src/main/resources/META-INF/services/io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeBuilder b/extensions/vertx-http/runtime/src/main/resources/META-INF/services/io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeBuilder new file mode 100644 index 0000000000000..d679c3d596966 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/resources/META-INF/services/io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeBuilder @@ -0,0 +1,29 @@ +io.quarkus.vertx.http.runtime.attribute.RemoteIPAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.LocalIPAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RequestProtocolAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.LocalPortAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.IdentUsernameAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RequestMethodAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.QueryStringAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RequestLineAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.BytesSentAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.DateTimeAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RemoteUserAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RequestURLAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.ThreadNameAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.LocalServerNameAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RequestHeaderAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.ResponseHeaderAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.CookieAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.ResponseCodeAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.QueryParameterAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.SslClientCertAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.SslCipherAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.SslSessionIdAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.ResponseTimeAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.TransportProtocolAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RequestSchemeAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.SecureExchangeAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RemoteHostAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.RequestPathAttribute$Builder +io.quarkus.vertx.http.runtime.attribute.NullAttribute$Builder