From 7ad1ee0addd2bff700bd4f1b0fff4636696b9211 Mon Sep 17 00:00:00 2001 From: Stephan Schnabel Date: Wed, 1 Dec 2021 10:25:11 +0100 Subject: [PATCH] Add config options to add authentication attributes as mdc. --- .yamllint | 3 - README.md | 175 +----------------- docs/build.md | 23 +++ docs/features/http_log_level.md | 35 ++++ docs/features/http_mdc_authentication.md | 29 +++ docs/features/http_mdc_headers.md | 28 +++ docs/features/logback_appender.md | 16 ++ docs/features/logback_default.md | 18 ++ docs/features/logback_mdc_level.md | 66 +++++++ pom.xml | 9 +- pom.xml.versionsBackup | 169 +++++++++++++++++ .../configurator/DefaultConfigurator.java | 3 + .../logging/http/AbstractMdcFilter.java | 47 +++++ .../level/LogLevelClientFilter.java} | 23 ++- .../level/LogLevelServerFilter.java} | 35 ++-- .../level/LogLevelTurboFilter.java} | 6 +- .../http/mdc/AuthenticationMdcFilter.java | 77 ++++++++ .../http/mdc/HttpHeadersMdcFilter.java | 58 ++++++ .../logging/request/PrincipalHttpFilter.java | 63 ------- .../micronaut/logging/AbstractTest.java | 7 +- .../logging/http/AbstractFilterTest.java | 133 +++++++++++++ .../http/level/LogLevelServerFilterTest.java | 80 ++++++++ .../http/mdc/AuthenticationMdcFilterTest.java | 73 ++++++++ .../http/mdc/HttpHeadersMdcFilterTest.java | 60 ++++++ .../logging/mdc/MDCTurboFilterTest.java | 2 +- .../logging/request/CompositeTest.java | 40 ---- .../logging/request/RequestHeaderTest.java | 56 ------ .../logging/request/RequestPrincipalTest.java | 43 ----- .../micronaut/logging/request/TestClient.java | 63 ------- .../logging/request/TestController.java | 54 ------ .../resources/application-test-composite.yaml | 8 - src/test/resources/application-test.yaml | 3 - 32 files changed, 964 insertions(+), 541 deletions(-) create mode 100644 docs/build.md create mode 100644 docs/features/http_log_level.md create mode 100644 docs/features/http_mdc_authentication.md create mode 100644 docs/features/http_mdc_headers.md create mode 100644 docs/features/logback_appender.md create mode 100644 docs/features/logback_default.md create mode 100644 docs/features/logback_mdc_level.md create mode 100644 pom.xml.versionsBackup create mode 100644 src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java rename src/main/java/io/kokuwa/micronaut/logging/{request/HeaderLoggingClientHttpFilter.java => http/level/LogLevelClientFilter.java} (62%) rename src/main/java/io/kokuwa/micronaut/logging/{request/HeaderLoggingServerHttpFilter.java => http/level/LogLevelServerFilter.java} (67%) rename src/main/java/io/kokuwa/micronaut/logging/{request/HeaderLoggingTurboFilter.java => http/level/LogLevelTurboFilter.java} (79%) create mode 100644 src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java create mode 100644 src/main/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilter.java delete mode 100644 src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java create mode 100644 src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java create mode 100644 src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java create mode 100644 src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java create mode 100644 src/test/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilterTest.java delete mode 100644 src/test/java/io/kokuwa/micronaut/logging/request/CompositeTest.java delete mode 100644 src/test/java/io/kokuwa/micronaut/logging/request/RequestHeaderTest.java delete mode 100644 src/test/java/io/kokuwa/micronaut/logging/request/RequestPrincipalTest.java delete mode 100644 src/test/java/io/kokuwa/micronaut/logging/request/TestClient.java delete mode 100644 src/test/java/io/kokuwa/micronaut/logging/request/TestController.java delete mode 100644 src/test/resources/application-test-composite.yaml diff --git a/.yamllint b/.yamllint index 9d1b12e..c9f12ef 100644 --- a/.yamllint +++ b/.yamllint @@ -5,6 +5,3 @@ rules: # no need for document start document-start: disable - - # line length is not important - line-length: disable diff --git a/README.md b/README.md index 9cf902e..620914e 100644 --- a/README.md +++ b/README.md @@ -1,176 +1,17 @@ # Micronaut Logging support -This branch is for Micronaut 2.x, for 3.x see wip branch [3.x](../../tree/3.x). - ## Features -### Default logback.xml +* [set log level based on MDC values](docs/features/logback_mdc_level.md) +* [add default xml](docs/features/logback_default.md) +* [preconfigured appender for different environments](docs/features/logback_appender.md) +* [set log level based on HTTP request header](docs/features/http_log_level.md) +* [add HTTP headers to MDC](docs/features/http_mdc_headers.md) +* [add authentication information from HTTP request to MDC](docs/features/http_mdc_authentication.md) -If no `logback.xml` by user is provided a default [logback.xml](src/main/resources/io/kokuwa/logback/logback-default.xml) is loaded. Otherwise use custom [logback.xml](src/main/resources/io/kokuwa/logback/logback-example.xml): +## Development -```xml - - - - - - - - -``` - -### Available Appender - -* console with jansi for developers -* gcp logging format (with support for error reporting) -* json - -### AutoSelect appender logback.xml - -1. if `LOGBACK_APPENDER` is set this appender will be used -2. if GCP is detected gcp appender will be used -3. if Kubernetes is detected json appender will be used -4. console appender else - -*IMPORTENT*: only works without custom `logback.xml` - -### Set log level based on MDC values - -Configuration: - -* *enabled*: enable MDC filter (`true` is default) -* *key*: MDC key, is optional (will use name instead, see example `user` below) -* *level*: log level to use (`TRACE` is default) -* *loggers*: passlist of logger names, matches all loggers if empty -* *values*: values for matching MDC key, matches all values if empty - -Example for setting different values for different values/logger: - -```yaml -logger: - levels: - io.kokuwa: INFO - mdc: - gateway-debug: - key: gateway - level: DEBUG - loggers: - - io.kokuwa - values: - - 6a1bae7f-eb6c-4c81-af9d-dc15396584e2 - - fb3318f1-2c73-48e9-acd4-a2be3c9f9256 - gateway-trace: - key: gateway - level: TRACE - loggers: - - io.kokuwa - - io.micronaut - values: - - 257802b2-22fe-4dcc-bb99-c1db2a47861f -``` - -Example for omiting level and key: - -```yaml -logger: - levels: - io.kokuwa: INFO - mdc: - gateway: - loggers: - - io.kokuwa - values: - - 257802b2-22fe-4dcc-bb99-c1db2a47861f - - 0a44738b-0c3a-4798-8210-2495485f10b2 -``` - -Example for minimal configuration: - -```yaml -logger: - levels: - io.kokuwa: INFO - mdc: - user: {} -``` - -### Set log level based on HTTP request header - -Configuration for server filter (prefixed with *logger.request.filter*): - -* *enabled*: enable HTTP server filter (`true` is default) -* *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v2.5.13/core/src/main/java/io/micronaut/core/order/Ordered.java) (highest is default) -* *path*: filter path (`/**` is default) -* *header*: name of HTTP header (`x-log-level` is default) - -Configuration for client filter for propagation (prefixed with *logger.request.propagation*): - -* *enabled*: enable HTTP client filter (`true` is default) -* *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v2.5.13/core/src/main/java/io/micronaut/core/order/Ordered.java) (tracing is default) -* *path*: filter path (`/**` is default) -* *header*: name of HTTP header (server header is default) - -Example with default configuration: - -```yaml -logger: - request: - filter: - enabled: true - order: -2147483648 - path: /** - header: x-log-level - propagation: - enabled: true - order: 19000 - path: /** - header: ${logger.request.header.header-name} -``` - -### Add principal for request to MDC - -Configuration: - -* *enabled*: enable HTTP principal filter (`true` is default) -* *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v2.5.13/core/src/main/java/io/micronaut/core/order/Ordered.java) ([ServerFilterPhase.SECURITY.after()](https://github.com/micronaut-projects/micronaut-core/blob/v2.5.13/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L54) is default) -* *path*: filter path (`/**` is default) -* *key*: name of MDC header (`principal` is default) - -Example with default configuration: - -```yaml -logger: - request: - principal: - enabled: true - order: 39250 - path: /** - key: principal -``` - -## Build & Release - -### Dependency updates - -Display dependency updates: - -```sh -mvn versions:display-property-updates -U -``` - -Update dependencies: - -```sh -mvn versions:update-properties -``` - -### Release locally - -Run: - -```sh -mvn release:prepare release:perform release:clean -B -DreleaseProfiles=oss-release -``` +* [build](docs/build.md) ## Open Topics diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 0000000..4d5c72e --- /dev/null +++ b/docs/build.md @@ -0,0 +1,23 @@ +# Build & Release + +## Dependency updates + +Display dependency updates: + +```sh +mvn versions:display-parent-updates versions:display-property-updates -U +``` + +Update dependencies: + +```sh +mvn versions:update-parent versions:update-properties +``` + +## Release locally + +Run: + +```sh +mvn release:prepare release:perform release:clean -B -DreleaseProfiles=oss-release +``` diff --git a/docs/features/http_log_level.md b/docs/features/http_log_level.md new file mode 100644 index 0000000..59b960d --- /dev/null +++ b/docs/features/http_log_level.md @@ -0,0 +1,35 @@ +# Set log level based on HTTP request header + +With this features it is possible to set the log level while processing a request by adding the http header `x-log-level` with value `TRACE`. This log level is propagated to HTTP client requests. + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.level.enabled` | filter enabled? | `true` +`logger.http.level.path` | filter path | `/**` +`logger.http.level.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.FIRST.before()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L34) +`logger.http.level.header` | name of HTTP header | `x-log-level` +`logger.http.level.propagation.enabled` | propagation enabled? | `true` +`logger.http.level.propagation.path` | propagation path | `/**` +`logger.http.level.propagation.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [Order.HIGHEST_PRECEDENCE](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java#L30) +`logger.http.level.propagation.header` | name of HTTP header | see `logger.http.level.header` + +## Examples + +Default configuration: + +```yaml +logger: + http: + level: + enabled: true + order: -1000 + path: /** + header: x-log-level + propagation: + enabled: true + order: 2147483648 + path: /** + header: ${logger.http.level.header} +``` diff --git a/docs/features/http_mdc_authentication.md b/docs/features/http_mdc_authentication.md new file mode 100644 index 0000000..ff4c4e9 --- /dev/null +++ b/docs/features/http_mdc_authentication.md @@ -0,0 +1,29 @@ +# Add authentication information to MDC + +This only applies to HTTP requests with successful security authentication. + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.authentication.enabled` | filter enabled? | `true` +`logger.http.authentication.path` | filter path | `/**` +`logger.http.authentication.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.SECURITY.after()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L54) +`logger.http.authentication.prefix` | prefix to MDC key | `` +`logger.http.authentication.name` | MDC key of authentication name | `principal` +`logger.http.authentication.attributes` | authentication attributes to add to MDC, | `[]` + +## Examples + +Configuration for adding some jwt claims: + +```yaml +logger: + http: + authentication: + prefix: jwt. + name: sub + attributes: + - aud + - azp +``` diff --git a/docs/features/http_mdc_headers.md b/docs/features/http_mdc_headers.md new file mode 100644 index 0000000..b6c031a --- /dev/null +++ b/docs/features/http_mdc_headers.md @@ -0,0 +1,28 @@ +# Add HTTP headers to MDC + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.headers.enabled` | filter enabled? | `true` +`logger.http.headers.path` | filter path | `/**` +`logger.http.headers.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.FIRST.before()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L34) +`logger.http.headers.prefix` | prefix to MDC key | `` +`logger.http.headers.names` | http header names to add to MDC | `[]` + +## Examples + +Configuration for b3-propagation: + +```yaml +logger: + http: + headers: + prefix: header. + names: + - x-request-id + - x-b3-traceId + - x-b3-parentspanid + - x-b3-spanid + - x-b3-sampled +``` diff --git a/docs/features/logback_appender.md b/docs/features/logback_appender.md new file mode 100644 index 0000000..1975e8b --- /dev/null +++ b/docs/features/logback_appender.md @@ -0,0 +1,16 @@ +# Appender + +## Available Appender + +* console with jansi for developers +* gcp logging format (with support for error reporting) +* json + +## AutoSelect appender + +1. if `LOGBACK_APPENDER` is set this appender will be used +2. if GCP is detected gcp appender will be used +3. if Kubernetes is detected json appender will be used +4. console appender else + +*IMPORTENT*: only works without custom `logback.xml` diff --git a/docs/features/logback_default.md b/docs/features/logback_default.md new file mode 100644 index 0000000..98ccbc5 --- /dev/null +++ b/docs/features/logback_default.md @@ -0,0 +1,18 @@ +# Add default logback.xml + +If no `logback.xml` by user is provided a default [logback.xml](../../src/main/resources/io/kokuwa/logback/logback-default.xml) is loaded. Otherwise use custom [logback.xml](../../src/main/resources/io/kokuwa/logback/logback-example.xml): + +```xml + + + + + + + + + + + + +``` diff --git a/docs/features/logback_mdc_level.md b/docs/features/logback_mdc_level.md new file mode 100644 index 0000000..53887d5 --- /dev/null +++ b/docs/features/logback_mdc_level.md @@ -0,0 +1,66 @@ +# Set log level based on MDC values + +This can be used to change the log level based on MDC valus. E.g. change log levels for specific users/services etc. + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.mdc.enabled` | MDC enabled? | `true` +`logger.mdc.` | MDC key to use | +`logger.mdc..key` | MDC key override, see complex example below for usage | `` +`logger.mdc..level` | log level to use | `TRACE` +`logger.mdc..loggers` | passlist of logger names, matches all loggers if empty | `[]` +`logger.mdc..values` | values for matching MDC key, matches all values if empty | `[]` + +## Examples + +Minimal configuration that logs everything with `TRACE` if MDC `principal` is present: + +```yaml +logger: + levels: + io.kokuwa: INFO + mdc: + principal: {} +``` + +Configuration that logs everything with `TRACE` for logger `io.kokuwa` if MDC `gateway` matches one value: + +```yaml +logger: + levels: + io.kokuwa: INFO + mdc: + gateway: + loggers: + - io.kokuwa + values: + - 257802b2-22fe-4dcc-bb99-c1db2a47861f + - 0a44738b-0c3a-4798-8210-2495485f10b2 +``` + +Complex example with setting different values for different values/logger: + +```yaml +logger: + levels: + io.kokuwa: INFO + mdc: + gateway-debug: + key: gateway + level: DEBUG + loggers: + - io.kokuwa + values: + - 6a1bae7f-eb6c-4c81-af9d-dc15396584e2 + - fb3318f1-2c73-48e9-acd4-a2be3c9f9256 + gateway-trace: + key: gateway + level: TRACE + loggers: + - io.kokuwa + - io.micronaut + values: + - 257802b2-22fe-4dcc-bb99-c1db2a47861f +``` diff --git a/pom.xml b/pom.xml index de101f4..ed38ada 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.kokuwa maven-parent - 0.5.4 + 0.5.5 @@ -49,7 +49,7 @@ 0.1.5 - 3.1.3 + 3.2.0 @@ -92,6 +92,11 @@ micronaut-runtime provided + + io.micronaut.security + micronaut-security + provided + io.micronaut.test micronaut-test-junit5 diff --git a/pom.xml.versionsBackup b/pom.xml.versionsBackup new file mode 100644 index 0000000..5a30895 --- /dev/null +++ b/pom.xml.versionsBackup @@ -0,0 +1,169 @@ + + + 4.0.0 + + + io.kokuwa + maven-parent + 0.5.4 + + + + io.kokuwa.micronaut + micronaut-logging + 3.0.0-SNAPSHOT + + Logging support for Micronaut + Enhanced logging using MDC or request header. + https://github.com/kokuwaio/micronaut-logging + 2020 + + + Apache License 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + + Stephan Schnabel + https://github.com/stephanschnabel + + + + + https://github.com/kokuwaio/micronaut-logging + scm:git:https://github.com/kokuwaio/micronaut-logging.git + scm:git:https://github.com/kokuwaio/micronaut-logging.git + HEAD + + + github + https://github.com/kokuwaio/micronaut-logging/issues + + + + + + + + + 0.1.5 + 3.2.0 + + + + + + + + + io.micronaut + micronaut-bom + ${version.io.micronaut} + pom + import + + + + + ch.qos.logback.contrib + logback-json-classic + ${version.ch.qos.logback.contrib} + + + ch.qos.logback.contrib + logback-json-core + ${version.ch.qos.logback.contrib} + + + ch.qos.logback.contrib + logback-jackson + ${version.ch.qos.logback.contrib} + + + + + + + + + io.micronaut + micronaut-runtime + provided + + + io.micronaut.security + micronaut-security + provided + + + io.micronaut.test + micronaut-test-junit5 + test + + + io.micronaut + micronaut-http-client + test + + + io.micronaut + micronaut-http-server-netty + test + + + io.micronaut.security + micronaut-security-jwt + test + + + + + com.google.code.findbugs + jsr305 + provided + + + + + ch.qos.logback + logback-classic + + + ch.qos.logback.contrib + logback-jackson + + + ch.qos.logback.contrib + logback-json-classic + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${version.org.projectlombok} + + + io.micronaut + micronaut-inject-java + ${version.io.micronaut} + + + + + + + + diff --git a/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java b/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java index 1ebef70..1d808a9 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java +++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java @@ -1,5 +1,6 @@ package io.kokuwa.micronaut.logging.configurator; +import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.Configurator; import ch.qos.logback.core.joran.spi.JoranException; @@ -29,5 +30,7 @@ public class DefaultConfigurator extends ContextAwareBase implements Configurato } catch (JoranException e) { addError("Failed to load logback.xml from io.kokuwa:micronaut-logging", e); } + + loggerContext.getLogger("io.micronaut.logging.PropertiesLoggingLevelsConfigurer").setLevel(Level.WARN); } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java new file mode 100644 index 0000000..777d363 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java @@ -0,0 +1,47 @@ +package io.kokuwa.micronaut.logging.http; + +import java.util.Map; + +import org.reactivestreams.Publisher; +import org.slf4j.MDC; + +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; + +/** + * Base for all MDC related http filters. + * + * @author Stephan Schnabel + */ +public abstract class AbstractMdcFilter implements HttpServerFilter { + + private final int order; + + public AbstractMdcFilter(Integer order) { + this.order = order; + } + + @Override + public int getOrder() { + return order; + } + + protected Publisher> doFilter( + HttpRequest request, + ServerFilterChain chain, + Map mdc) { + + if (mdc.isEmpty()) { + return chain.proceed(request); + } + + mdc.forEach(MDC::put); + return Publishers.map(chain.proceed(request), response -> { + mdc.keySet().forEach(MDC::remove); + return response; + }); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingClientHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java similarity index 62% rename from src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingClientHttpFilter.java rename to src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java index 463a070..511829d 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingClientHttpFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java @@ -1,4 +1,4 @@ -package io.kokuwa.micronaut.logging.request; +package io.kokuwa.micronaut.logging.http.level; import java.util.Optional; @@ -13,30 +13,29 @@ import io.micronaut.http.annotation.Filter; import io.micronaut.http.context.ServerRequestContext; import io.micronaut.http.filter.ClientFilterChain; import io.micronaut.http.filter.HttpClientFilter; -import io.micronaut.http.filter.ServerFilterPhase; /** - * Http request logging filter. + * Propagates log-level from server request to client. * * @author Stephan Schnabel */ -@Requires(beans = HeaderLoggingServerHttpFilter.class) -@Requires(property = HeaderLoggingClientHttpFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) -@Filter("${" + HeaderLoggingClientHttpFilter.PREFIX + ".path:/**}") -public class HeaderLoggingClientHttpFilter implements HttpClientFilter { +@Requires(beans = LogLevelServerFilter.class) +@Requires(property = LogLevelClientFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Filter("${" + LogLevelClientFilter.PREFIX + ".path:/**}") +public class LogLevelClientFilter implements HttpClientFilter { - public static final String PREFIX = "logger.request.propagation"; - public static final int DEFAULT_ORDER = ServerFilterPhase.TRACING.order(); + public static final String PREFIX = "logger.http.level.propagation"; + public static final int DEFAULT_ORDER = HIGHEST_PRECEDENCE; private final String serverHeader; private final String propagationHeader; private final int order; - public HeaderLoggingClientHttpFilter( - @Value("${" + HeaderLoggingServerHttpFilter.PREFIX + ".header}") Optional serverHeader, + public LogLevelClientFilter( + @Value("${" + LogLevelServerFilter.PREFIX + ".header}") Optional serverHeader, @Value("${" + PREFIX + ".header}") Optional propagationHeader, @Value("${" + PREFIX + ".order}") Optional order) { - this.serverHeader = serverHeader.orElse(HeaderLoggingServerHttpFilter.DEFAULT_HEADER); + this.serverHeader = serverHeader.orElse(LogLevelServerFilter.DEFAULT_HEADER); this.propagationHeader = propagationHeader.orElse(this.serverHeader); this.order = order.orElse(DEFAULT_ORDER); } diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingServerHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java similarity index 67% rename from src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingServerHttpFilter.java rename to src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java index f0afce3..67357ed 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingServerHttpFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java @@ -1,4 +1,4 @@ -package io.kokuwa.micronaut.logging.request; +package io.kokuwa.micronaut.logging.http.level; import java.util.Optional; @@ -10,6 +10,7 @@ import org.slf4j.MDC; import ch.qos.logback.classic.turbo.TurboFilter; import io.kokuwa.micronaut.logging.LogbackUtil; +import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; import io.micronaut.context.annotation.Requires; import io.micronaut.context.annotation.Value; import io.micronaut.core.async.publisher.Publishers; @@ -17,54 +18,46 @@ import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.annotation.Filter; -import io.micronaut.http.filter.HttpServerFilter; import io.micronaut.http.filter.ServerFilterChain; import io.micronaut.http.filter.ServerFilterPhase; -import io.micronaut.runtime.server.EmbeddedServer; +import io.micronaut.runtime.context.scope.Refreshable; /** * Http request logging filter. * * @author Stephan Schnabel */ -@Requires(beans = EmbeddedServer.class) -@Requires(property = HeaderLoggingServerHttpFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) -@Filter("${" + HeaderLoggingServerHttpFilter.PREFIX + ".path:/**}") -public class HeaderLoggingServerHttpFilter implements HttpServerFilter { - - public static final String PREFIX = "logger.request.filter"; - public static final String MDC_FILTER = PREFIX; - public static final String MDC_KEY = "level"; +@Refreshable +@Requires(property = LogLevelServerFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Filter("${" + LogLevelServerFilter.PREFIX + ".path:/**}") +public class LogLevelServerFilter extends AbstractMdcFilter { + public static final String PREFIX = "logger.http.level"; public static final String DEFAULT_HEADER = "x-log-level"; public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); + public static final String MDC_KEY = "level"; + public static final String MDC_FILTER = PREFIX; private final LogbackUtil logback; private final String header; - private final int order; - public HeaderLoggingServerHttpFilter( + public LogLevelServerFilter( LogbackUtil logback, @Value("${" + PREFIX + ".header}") Optional header, @Value("${" + PREFIX + ".order}") Optional order) { + super(order.orElse(DEFAULT_ORDER)); this.logback = logback; this.header = header.orElse(DEFAULT_HEADER); - this.order = order.orElse(DEFAULT_ORDER); } @PostConstruct void startTurbofilter() { - logback.getTurboFilter(HeaderLoggingTurboFilter.class, MDC_FILTER, HeaderLoggingTurboFilter::new).start(); + logback.getTurboFilter(LogLevelTurboFilter.class, MDC_FILTER, LogLevelTurboFilter::new).start(); } @PreDestroy void stopTurbofilter() { - logback.getTurboFilter(HeaderLoggingTurboFilter.class, MDC_FILTER).ifPresent(TurboFilter::stop); - } - - @Override - public int getOrder() { - return order; + logback.getTurboFilter(LogLevelTurboFilter.class, MDC_FILTER).ifPresent(TurboFilter::stop); } @Override diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelTurboFilter.java similarity index 79% rename from src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingTurboFilter.java rename to src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelTurboFilter.java index c1a8dc3..57df67f 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingTurboFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelTurboFilter.java @@ -1,4 +1,4 @@ -package io.kokuwa.micronaut.logging.request; +package io.kokuwa.micronaut.logging.http.level; import org.slf4j.MDC; import org.slf4j.Marker; @@ -13,7 +13,7 @@ import ch.qos.logback.core.spi.FilterReply; * * @author Stephan Schnabel */ -public class HeaderLoggingTurboFilter extends TurboFilter { +public class LogLevelTurboFilter extends TurboFilter { @Override public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { @@ -22,7 +22,7 @@ public class HeaderLoggingTurboFilter extends TurboFilter { return FilterReply.NEUTRAL; } - var value = MDC.get(HeaderLoggingServerHttpFilter.MDC_KEY); + var value = MDC.get(LogLevelServerFilter.MDC_KEY); if (value == null) { return FilterReply.NEUTRAL; } diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java new file mode 100644 index 0000000..f1c4a14 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java @@ -0,0 +1,77 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +import org.reactivestreams.Publisher; +import org.slf4j.MDC; + +import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.filter.ServerFilterPhase; +import io.micronaut.runtime.context.scope.Refreshable; +import io.micronaut.security.authentication.Authentication; + +/** + * Filter to add claims from authentication to MDC. + * + * @author Stephan Schnabel + */ +@Refreshable +@Requires(classes = Authentication.class) +@Requires(property = AuthenticationMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Filter("${" + AuthenticationMdcFilter.PREFIX + ".path:/**}") +public class AuthenticationMdcFilter extends AbstractMdcFilter { + + public static final String PREFIX = "logger.http.authentication"; + public static final String DEFAULT_NAME = "principal"; + public static final int DEFAULT_ORDER = ServerFilterPhase.SECURITY.after(); + + private final String name; + private final List attributes; + private final String prefix; + + public AuthenticationMdcFilter( + @Value("${" + PREFIX + ".name:principal}") Optional name, + @Value("${" + PREFIX + ".attributes:[]}") List attributes, + @Value("${" + PREFIX + ".prefix}") Optional prefix, + @Value("${" + PREFIX + ".order}") Optional order) { + super(order.orElse(DEFAULT_ORDER)); + this.name = name.orElse(DEFAULT_NAME); + this.prefix = prefix.orElse(null); + this.attributes = attributes; + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + + // get authentication + + var optional = request.getUserPrincipal(Authentication.class); + if (optional.isEmpty()) { + return chain.proceed(request); + } + var authentication = optional.get(); + var authenticationAttributes = authentication.getAttributes(); + + // add mdc + + var mdc = new HashMap(); + MDC.put(prefix == null ? name : prefix + name, authentication.getName()); + for (var header : attributes) { + var value = authenticationAttributes.get(header); + if (value != null) { + mdc.put(prefix == null ? header : prefix + header, String.valueOf(value)); + } + } + + return doFilter(request, chain, mdc); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilter.java new file mode 100644 index 0000000..efca100 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilter.java @@ -0,0 +1,58 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; + +import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.filter.ServerFilterPhase; +import io.micronaut.runtime.context.scope.Refreshable; + +/** + * Filter to add http headers to MDC. + * + * @author Stephan Schnabel + */ +@Refreshable +@Requires(property = HttpHeadersMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(property = HttpHeadersMdcFilter.PREFIX + ".names") +@Filter("${" + HttpHeadersMdcFilter.PREFIX + ".path:/**}") +public class HttpHeadersMdcFilter extends AbstractMdcFilter { + + public static final String PREFIX = "logger.http.headers"; + public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); + + private final Set headers; + private final String prefix; + + public HttpHeadersMdcFilter( + @Value("${" + PREFIX + ".names}") List headers, + @Value("${" + PREFIX + ".prefix}") Optional prefix, + @Value("${" + PREFIX + ".order}") Optional order) { + super(order.orElse(DEFAULT_ORDER)); + this.prefix = prefix.orElse(null); + this.headers = headers.stream().map(String::toLowerCase).collect(Collectors.toSet()); + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + var mdc = new HashMap(); + for (var header : headers) { + request.getHeaders() + .getFirst(header) + .ifPresent(value -> mdc.put(prefix == null ? header : prefix + header, String.valueOf(value))); + } + return doFilter(request, chain, mdc); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java deleted file mode 100644 index 0e7a14d..0000000 --- a/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import java.util.Optional; - -import org.reactivestreams.Publisher; -import org.slf4j.MDC; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.annotation.Value; -import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.util.StringUtils; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Filter; -import io.micronaut.http.filter.HttpServerFilter; -import io.micronaut.http.filter.ServerFilterChain; -import io.micronaut.http.filter.ServerFilterPhase; -import io.micronaut.runtime.server.EmbeddedServer; - -/** - * Http request principal filter. - * - * @author Stephan Schnabel - */ -@Requires(beans = EmbeddedServer.class) -@Requires(property = PrincipalHttpFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) -@Filter("${" + PrincipalHttpFilter.PREFIX + ".path:/**}") -public class PrincipalHttpFilter implements HttpServerFilter { - - public static final String PREFIX = "logger.request.principal"; - - public static final String DEFAULT_KEY = "principal"; - public static final int DEFAULT_ORDER = ServerFilterPhase.SECURITY.after(); - - private final String key; - private final int order; - - public PrincipalHttpFilter( - @Value("${" + PREFIX + ".key:" + DEFAULT_KEY + "}") String key, - @Value("${" + PREFIX + ".order}") Optional order) { - this.key = key; - this.order = order.orElse(DEFAULT_ORDER); - } - - @Override - public int getOrder() { - return order; - } - - @Override - public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - var princial = request.getUserPrincipal(); - if (princial.isPresent()) { - MDC.put(key, princial.get().getName()); - return Publishers.map(chain.proceed(request), response -> { - MDC.remove(key); - return response; - }); - } else { - return chain.proceed(request); - } - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java index 53960e4..366ba0c 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java @@ -2,7 +2,9 @@ package io.kokuwa.micronaut.logging; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer.DisplayName; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.TestClassOrder; import org.junit.jupiter.api.TestMethodOrder; import org.slf4j.MDC; @@ -14,7 +16,8 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest; * @author Stephan Schnabel */ @MicronautTest -@TestMethodOrder(DisplayName.class) +@TestClassOrder(ClassOrderer.DisplayName.class) +@TestMethodOrder(MethodOrderer.DisplayName.class) public abstract class AbstractTest { @BeforeEach diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java new file mode 100644 index 0000000..11d6676 --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java @@ -0,0 +1,133 @@ +package io.kokuwa.micronaut.logging.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.MDC; + +import com.nimbusds.jwt.JWTClaimsSet; + +import ch.qos.logback.classic.Level; +import io.kokuwa.micronaut.logging.AbstractTest; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpHeaderValues; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.client.DefaultHttpClientConfiguration; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.runtime.server.EmbeddedServer; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.security.token.jwt.signature.SignatureGeneratorConfiguration; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +/** + * Test for {@link HttpServerFilter}. + * + * @author Stephan Schnabel + */ +@MicronautTest(rebuildContext = true) +public abstract class AbstractFilterTest extends AbstractTest { + + private static boolean INIT = false; + + @Inject + SignatureGeneratorConfiguration signature; + @Inject + EmbeddedServer embeddedServer; + + @DisplayName("0 init") + @Test + @BeforeEach + void refresh() { + // https://github.com/micronaut-projects/micronaut-core/issues/5453#issuecomment-864594741 + if (INIT) { + embeddedServer.refresh(); + } else { + INIT = true; + } + } + + // security + + public String token(String subject) { + return token(subject, claims -> {}); + } + + @SneakyThrows + public String token(String subject, Consumer manipulator) { + var claims = new JWTClaimsSet.Builder().subject(subject); + manipulator.accept(claims); + return HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER + " " + signature.sign(claims.build()).serialize(); + } + + // request + + @SneakyThrows + public TestResponse get(Map headers) { + + var request = HttpRequest.GET("/"); + headers.forEach((name, value) -> request.header(name, value)); + var configuration = new DefaultHttpClientConfiguration(); + configuration.setLoggerName("io.kokuwa.TestClient"); + var response = HttpClient + .create(embeddedServer.getURL(), configuration) + .toBlocking().exchange(request, TestResponse.class); + assertEquals(HttpStatus.OK, response.getStatus(), "status"); + assertTrue(response.getBody().isPresent(), "body"); + assertTrue(CollectionUtils.isEmpty(MDC.getCopyOfContextMap()), "mdc leaked: " + MDC.getCopyOfContextMap()); + + return response.body(); + } + + @Secured({ SecurityRule.IS_ANONYMOUS, SecurityRule.IS_AUTHENTICATED }) + @Controller + @Slf4j + public static class TestController { + + @Get("/") + TestResponse run() { + + var level = Level.OFF; + if (log.isTraceEnabled()) { + level = Level.TRACE; + } else if (log.isDebugEnabled()) { + level = Level.DEBUG; + } else if (log.isInfoEnabled()) { + level = Level.INFO; + } else if (log.isWarnEnabled()) { + level = Level.WARN; + } else if (log.isErrorEnabled()) { + level = Level.ERROR; + } + + var mdc = MDC.getCopyOfContextMap(); + log.info("Found MDC: {}", mdc); + + return new TestResponse(level.toString(), mdc == null ? Map.of() : mdc); + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class TestResponse { + private String level; + private Map context = Map.of(); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java new file mode 100644 index 0000000..b25a6af --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java @@ -0,0 +1,80 @@ +package io.kokuwa.micronaut.logging.http.level; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ch.qos.logback.classic.Level; +import io.kokuwa.micronaut.logging.http.AbstractFilterTest; +import io.micronaut.context.annotation.Property; + +/** + * Test for {@link LogLevelServerFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("http: set log level via http request") +public class LogLevelServerFilterTest extends AbstractFilterTest { + + @DisplayName("noop: disabled") + @Test + @Property(name = "logger.http.level.enabled", value = "false") + void noopDisabled() { + assertLevel(Level.INFO, "TRACE"); + } + + @DisplayName("noop: header missing") + @Test + void noopHeaderMissing() { + assertLevel(Level.INFO, null); + } + + @DisplayName("noop: header invalid, use DEBUG as default from logback") + @Test + void noopHeaderInvalid() { + assertLevel(Level.DEBUG, "TRCE"); + } + + @DisplayName("level: trace (below default)") + @Test + void levelTrace() { + assertLevel(Level.TRACE, "TRACE"); + } + + @DisplayName("level: debug (below default)") + @Test + void levelDebug() { + assertLevel(Level.DEBUG, "DEBUG"); + } + + @DisplayName("level: info (is default)") + @Test + void levelInfo() { + assertLevel(Level.INFO, "INFO"); + } + + @DisplayName("level: warn (above default)") + @Test + void levelWarn() { + assertLevel(Level.INFO, "WARN"); + } + + @DisplayName("config: custom header name") + @Test + @Property(name = "logger.http.level.header", value = "FOO") + void configHeaderWarn() { + assertLevel(Level.TRACE, "FOO", "TRACE"); + } + + private void assertLevel(Level expectedLevel, String value) { + assertLevel(expectedLevel, LogLevelServerFilter.DEFAULT_HEADER, value); + } + + private void assertLevel(Level expectedLevel, String name, String value) { + var headers = value == null ? Map.of() : Map.of(name, value); + assertEquals(expectedLevel.toString(), get(headers).getLevel()); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java new file mode 100644 index 0000000..0492139 --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java @@ -0,0 +1,73 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.kokuwa.micronaut.logging.http.AbstractFilterTest; +import io.micronaut.context.annotation.Property; +import io.micronaut.http.HttpHeaders; + +/** + * Test for {@link AuthenticationMdcFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("http: mdc from authentication") +public class AuthenticationMdcFilterTest extends AbstractFilterTest { + + @DisplayName("noop: disabled") + @Test + @Property(name = "logger.http.authentication.enabled", value = "false") + void noopDisabled() { + assertEquals(Map.of(), getContext(true)); + } + + @DisplayName("noop: token missing") + @Test + void noopTokenMissing() { + assertEquals(Map.of(), getContext(false)); + } + + @DisplayName("mdc: default config") + @Test + void mdcWithDefault() { + assertEquals(Map.of("principal", "mySubject"), getContext(true)); + } + + @DisplayName("mdc: with name") + @Test + @Property(name = "logger.http.authentication.name", value = "sub") + void mdcWithName() { + assertEquals(Map.of("sub", "mySubject"), getContext(true)); + } + + @DisplayName("mdc: with attribute keys") + @Test + @Property(name = "logger.http.authentication.attributes", value = "azp,aud") + void mdcWithAttributes() { + assertEquals(Map.of("principal", "mySubject", "aud", "[a, b]", "azp", "myAzp"), getContext(true)); + } + + @DisplayName("mdc: with prefix") + @Test + @Property(name = "logger.http.authentication.name", value = "sub") + @Property(name = "logger.http.authentication.attributes", value = "azp") + @Property(name = "logger.http.authentication.prefix", value = "auth.") + void mdcWithPrefix() { + assertEquals(Map.of("auth.sub", "mySubject", "auth.azp", "myAzp"), getContext(true)); + } + + private Map getContext(boolean token) { + return get(token + ? Map.of(HttpHeaders.AUTHORIZATION, token("mySubject", claims -> claims + .issuer("nope") + .claim("azp", "myAzp") + .audience(List.of("a", "b")))) + : Map.of()).getContext(); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilterTest.java new file mode 100644 index 0000000..f551bd9 --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilterTest.java @@ -0,0 +1,60 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.kokuwa.micronaut.logging.http.AbstractFilterTest; +import io.micronaut.context.annotation.Property; + +/** + * Test for {@link HttpHeadersMdcFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("http: mdc from headers") +public class HttpHeadersMdcFilterTest extends AbstractFilterTest { + + @DisplayName("noop: empty configuration") + @Test + void noopEmptyConfiguration() { + assertContext(Map.of(), Map.of("foo", "bar")); + } + + @DisplayName("noop: disabled") + @Test + @Property(name = "logger.http.headers.enabled", value = "false") + @Property(name = "logger.http.headers.names", value = "foo") + void noopDisabled() { + assertContext(Map.of(), Map.of("foo", "bar")); + } + + @DisplayName("mdc: mismatch") + @Test + @Property(name = "logger.http.headers.names", value = "foo") + void mdcMismatch() { + assertContext(Map.of(), Map.of("nope", "bar")); + } + + @DisplayName("mdc: match without prefix") + @Test + @Property(name = "logger.http.headers.names", value = "foo") + void mdcMatchWithoutPrefix() { + assertContext(Map.of("foo", "bar"), Map.of("foo", "bar", "nope", "bar")); + } + + @DisplayName("mdc: match with prefix") + @Test + @Property(name = "logger.http.headers.names", value = "foo") + @Property(name = "logger.http.headers.prefix", value = "header.") + void mdcMatchWithPrefix() { + assertContext(Map.of("header.foo", "bar"), Map.of("foo", "bar", "nope", "bar")); + } + + private void assertContext(Map expectedMdcs, Map headers) { + assertEquals(expectedMdcs, get(headers).getContext()); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java index 5fc80fa..186c2a3 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java @@ -17,7 +17,7 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest; * * @author Stephan Schnabel */ -@DisplayName("mdc") +@DisplayName("mdc based log levels") @MicronautTest(environments = "test-mdc") public class MDCTurboFilterTest extends AbstractTest { diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/CompositeTest.java b/src/test/java/io/kokuwa/micronaut/logging/request/CompositeTest.java deleted file mode 100644 index 2c69814..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/CompositeTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import ch.qos.logback.classic.Level; -import io.kokuwa.micronaut.logging.AbstractTest; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import jakarta.inject.Inject; - -/** - * Test for MDC and request filter combined. - * - * @author Stephan Schnabel - */ -@DisplayName("request-composite") -@MicronautTest(environments = "test-composite") -public class CompositeTest extends AbstractTest { - - @Inject - TestClient client; - - @DisplayName("default level") - @Test - void defaultLogging() { - client.assertLevel(Level.INFO, client.token("somebody"), null); - } - - @DisplayName("level set by mdc") - @Test - void headerFromMdc() { - client.assertLevel(Level.DEBUG, client.token("horst"), null); - } - - @DisplayName("level set by header (overriding mdc)") - @Test - void headerFromHeader() { - client.assertLevel(Level.TRACE, client.token("horst"), "TRACE"); - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/RequestHeaderTest.java b/src/test/java/io/kokuwa/micronaut/logging/request/RequestHeaderTest.java deleted file mode 100644 index 476e976..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/RequestHeaderTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import ch.qos.logback.classic.Level; -import io.kokuwa.micronaut.logging.AbstractTest; -import jakarta.inject.Inject; - -/** - * Test for {@link HeaderLoggingServerHttpFilter}. - * - * @author Stephan Schnabel - */ -@DisplayName("request-header") -public class RequestHeaderTest extends AbstractTest { - - @Inject - TestClient client; - - @DisplayName("header missing") - @Test - void headerMissing() { - client.assertLevel(Level.INFO, null, null); - } - - @DisplayName("header invalid, use DEBUG as default from logback") - @Test - void headerInvalid() { - client.assertLevel(Level.DEBUG, null, "TRCE"); - } - - @DisplayName("level trace (below default)") - @Test - void headerLevelTrace() { - client.assertLevel(Level.TRACE, null, "TRACE"); - } - - @DisplayName("level debug (below default)") - @Test - void headerLevelDebug() { - client.assertLevel(Level.DEBUG, null, "DEBUG"); - } - - @DisplayName("level info (is default)") - @Test - void headerLevelInfo() { - client.assertLevel(Level.INFO, null, "INFO"); - } - - @DisplayName("level warn (above default)") - @Test - void headerLevelWarn() { - client.assertLevel(Level.INFO, null, "WARN"); - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/RequestPrincipalTest.java b/src/test/java/io/kokuwa/micronaut/logging/request/RequestPrincipalTest.java deleted file mode 100644 index 9a60d7c..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/RequestPrincipalTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import io.kokuwa.micronaut.logging.AbstractTest; -import jakarta.inject.Inject; - -/** - * Test for {@link PrincipalHttpFilter}. - * - * @author Stephan Schnabel - */ -@DisplayName("request-principal") -public class RequestPrincipalTest extends AbstractTest { - - @Inject - TestClient client; - - @DisplayName("token missing") - @Test - void tokenMissing() { - assertPrincipal(null, null); - } - - @DisplayName("token invalid") - @Test - void tokenInvalid() { - assertPrincipal(null, "meh"); - } - - @DisplayName("token valid") - @Test - void tokenValid() { - assertPrincipal("meh", client.token("meh")); - } - - private void assertPrincipal(String expectedPrincipal, String actualTokenValue) { - assertEquals(expectedPrincipal, client.get(actualTokenValue, null).getPrincipal()); - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/TestClient.java b/src/test/java/io/kokuwa/micronaut/logging/request/TestClient.java deleted file mode 100644 index edebb40..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/TestClient.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jwt.JWTClaimsSet; - -import ch.qos.logback.classic.Level; -import io.kokuwa.micronaut.logging.request.TestController.TestResponse; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.annotation.Client; -import io.micronaut.security.token.jwt.signature.SignatureGeneratorConfiguration; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - -/** - * Contoller for testing {@link HeaderLoggingServerHttpFilter} and {@link PrincipalHttpFilter}. - * - * @author Stephan Schnabel - */ -@Singleton -public class TestClient { - - @Inject - @Client("/") - HttpClient client; - @Inject - SignatureGeneratorConfiguration signature; - - String token(String subject) { - try { - return signature.sign(new JWTClaimsSet.Builder().subject(subject).build()).serialize(); - } catch (JOSEException e) { - fail("failed to create token"); - return null; - } - } - - TestResponse get(String token, String header) { - - var request = HttpRequest.GET("/"); - if (token != null) { - request.bearerAuth(token); - } - if (header != null) { - request.getHeaders().add(HeaderLoggingServerHttpFilter.DEFAULT_HEADER, header); - } - - var response = client.toBlocking().exchange(request, TestResponse.class); - assertEquals(HttpStatus.OK, response.getStatus(), "status"); - assertTrue(response.getBody().isPresent(), "body"); - - return response.body(); - } - - void assertLevel(Level expectedLevel, String actualTokenValue, String actualHeaderValue) { - assertEquals(expectedLevel.toString(), get(actualTokenValue, actualHeaderValue).getLevel()); - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/TestController.java b/src/test/java/io/kokuwa/micronaut/logging/request/TestController.java deleted file mode 100644 index d179f63..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/TestController.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import org.slf4j.MDC; - -import ch.qos.logback.classic.Level; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.security.annotation.Secured; -import io.micronaut.security.rules.SecurityRule; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * Controller for testing {@link HeaderLoggingServerHttpFilter} and {@link PrincipalHttpFilter}. - * - * @author Stephan Schnabel - */ -@Secured({ SecurityRule.IS_ANONYMOUS, SecurityRule.IS_AUTHENTICATED }) -@Controller -@Slf4j -public class TestController { - - @Get("/") - TestResponse run() { - - var principal = MDC.get(PrincipalHttpFilter.DEFAULT_KEY); - var level = Level.OFF; - if (log.isTraceEnabled()) { - level = Level.TRACE; - } else if (log.isDebugEnabled()) { - level = Level.DEBUG; - } else if (log.isInfoEnabled()) { - level = Level.INFO; - } else if (log.isWarnEnabled()) { - level = Level.WARN; - } else if (log.isErrorEnabled()) { - level = Level.ERROR; - } - - log.info("Test log for MDC inclusion, expected: {}", principal); - - return new TestResponse(level.toString(), principal); - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class TestResponse { - private String level; - private String principal; - } -} diff --git a/src/test/resources/application-test-composite.yaml b/src/test/resources/application-test-composite.yaml deleted file mode 100644 index d15a885..0000000 --- a/src/test/resources/application-test-composite.yaml +++ /dev/null @@ -1,8 +0,0 @@ -logger: - mdc: - principal: - level: DEBUG - loggers: - - io.kokuwa - values: - - horst diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 71beb2c..867b23e 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -7,6 +7,3 @@ micronaut: generator: secret: pleaseChangeThisSecretForANewOne jws-algorithm: HS256 - http: - client: - logger-name: io.kokuwa.Test