Add path mdc filter.
This commit is contained in:
parent
7403b04efd
commit
82ba578dc2
8 changed files with 228 additions and 8 deletions
|
@ -6,6 +6,7 @@
|
||||||
* [add default xml](docs/features/logback_default.md)
|
* [add default xml](docs/features/logback_default.md)
|
||||||
* [preconfigured appender for different environments](docs/features/logback_appender.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)
|
* [set log level based on HTTP request header](docs/features/http_log_level.md)
|
||||||
|
* [add HTTP path parts to MDC](docs/features/http_mdc_path.md)
|
||||||
* [add HTTP header to MDC](docs/features/http_mdc_header.md)
|
* [add HTTP header to MDC](docs/features/http_mdc_header.md)
|
||||||
* [add authentication information from HTTP request to MDC](docs/features/http_mdc_authentication.md)
|
* [add authentication information from HTTP request to MDC](docs/features/http_mdc_authentication.md)
|
||||||
|
|
||||||
|
|
25
docs/features/http_mdc_path.md
Normal file
25
docs/features/http_mdc_path.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Add HTTP path parts to MDC
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Property | Description | Default
|
||||||
|
-------- | ----------- | -------
|
||||||
|
`logger.http.path.enabled` | filter enabled? | `true`
|
||||||
|
`logger.http.path.path` | filter path | `/**`
|
||||||
|
`logger.http.path.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.path.prefix` | prefix to MDC key | ``
|
||||||
|
`logger.http.path.patterns` | patterns with groups to add to MDC | `[]`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Configuration for adding ids:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logger:
|
||||||
|
http:
|
||||||
|
path:
|
||||||
|
prefix: path.
|
||||||
|
patterns:
|
||||||
|
- \/gateway\/(?<gatewayId>[a-f0-9\-]{36})
|
||||||
|
- \/gateway\/(?<gatewayId>[a-f0-9\-]{36})\/configuration\/(?<config>[a-z]+)
|
||||||
|
```
|
|
@ -0,0 +1,88 @@
|
||||||
|
package io.kokuwa.micronaut.logging.http.mdc;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.regex.PatternSyntaxException;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter to add request path parts to MDC.
|
||||||
|
*
|
||||||
|
* @author Stephan Schnabel
|
||||||
|
*/
|
||||||
|
@Refreshable
|
||||||
|
@Requires(property = PathMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE)
|
||||||
|
@Requires(property = PathMdcFilter.PREFIX + ".patterns")
|
||||||
|
@Filter("${" + PathMdcFilter.PREFIX + ".path:/**}")
|
||||||
|
@Slf4j
|
||||||
|
public class PathMdcFilter extends AbstractMdcFilter {
|
||||||
|
|
||||||
|
public static final String PREFIX = "logger.http.path";
|
||||||
|
public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before();
|
||||||
|
public static final Pattern PATTERN_GROUPS = Pattern.compile("\\(\\?<([a-zA-Z][a-zA-Z0-9]+)>");
|
||||||
|
|
||||||
|
private final Map<Pattern, Set<String>> patternsWithGroups;
|
||||||
|
|
||||||
|
public PathMdcFilter(
|
||||||
|
@Value("${" + PREFIX + ".patterns}") List<String> patterns,
|
||||||
|
@Value("${" + PREFIX + ".prefix}") Optional<String> prefix,
|
||||||
|
@Value("${" + PREFIX + ".order}") Optional<Integer> order) {
|
||||||
|
super(order.orElse(DEFAULT_ORDER), prefix.orElse(null));
|
||||||
|
this.patternsWithGroups = new HashMap<>();
|
||||||
|
for (var patternString : patterns) {
|
||||||
|
try {
|
||||||
|
var pattern = Pattern.compile(patternString);
|
||||||
|
var groupMatcher = PATTERN_GROUPS.matcher(pattern.toString());
|
||||||
|
var groups = new HashSet<String>();
|
||||||
|
while (groupMatcher.find()) {
|
||||||
|
groups.add(groupMatcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groups.isEmpty()) {
|
||||||
|
log.warn("Path {} is missing groups.", patternString);
|
||||||
|
} else {
|
||||||
|
log.info("Added path {} with groups {}.", patternString, groups);
|
||||||
|
patternsWithGroups.put(pattern, groups);
|
||||||
|
}
|
||||||
|
} catch (PatternSyntaxException e) {
|
||||||
|
log.warn("Path {} is invalid.", patternString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
|
||||||
|
|
||||||
|
var mdc = new HashMap<String, String>();
|
||||||
|
var path = request.getPath();
|
||||||
|
|
||||||
|
for (var patternWithGroup : patternsWithGroups.entrySet()) {
|
||||||
|
var matcher = patternWithGroup.getKey().matcher(path);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
for (var group : patternWithGroup.getValue()) {
|
||||||
|
mdc.put(group, matcher.group(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFilter(request, chain, mdc);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import io.micronaut.http.HttpRequest;
|
||||||
import io.micronaut.http.HttpStatus;
|
import io.micronaut.http.HttpStatus;
|
||||||
import io.micronaut.http.annotation.Controller;
|
import io.micronaut.http.annotation.Controller;
|
||||||
import io.micronaut.http.annotation.Get;
|
import io.micronaut.http.annotation.Get;
|
||||||
|
import io.micronaut.http.annotation.PathVariable;
|
||||||
import io.micronaut.http.client.DefaultHttpClientConfiguration;
|
import io.micronaut.http.client.DefaultHttpClientConfiguration;
|
||||||
import io.micronaut.http.client.HttpClient;
|
import io.micronaut.http.client.HttpClient;
|
||||||
import io.micronaut.http.filter.HttpServerFilter;
|
import io.micronaut.http.filter.HttpServerFilter;
|
||||||
|
@ -79,9 +80,9 @@ public abstract class AbstractFilterTest extends AbstractTest {
|
||||||
// request
|
// request
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public TestResponse get(Map<String, String> headers) {
|
public TestResponse get(String path, Map<String, String> headers) {
|
||||||
|
|
||||||
var request = HttpRequest.GET("/");
|
var request = HttpRequest.GET(path);
|
||||||
headers.forEach((name, value) -> request.header(name, value));
|
headers.forEach((name, value) -> request.header(name, value));
|
||||||
var configuration = new DefaultHttpClientConfiguration();
|
var configuration = new DefaultHttpClientConfiguration();
|
||||||
configuration.setLoggerName("io.kokuwa.TestClient");
|
configuration.setLoggerName("io.kokuwa.TestClient");
|
||||||
|
@ -100,8 +101,8 @@ public abstract class AbstractFilterTest extends AbstractTest {
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public static class TestController {
|
public static class TestController {
|
||||||
|
|
||||||
@Get("/")
|
@Get("/{+path}")
|
||||||
TestResponse run() {
|
TestResponse run(@PathVariable String path) {
|
||||||
|
|
||||||
var level = Level.OFF;
|
var level = Level.OFF;
|
||||||
if (log.isTraceEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
|
@ -119,7 +120,7 @@ public abstract class AbstractFilterTest extends AbstractTest {
|
||||||
var mdc = MDC.getCopyOfContextMap();
|
var mdc = MDC.getCopyOfContextMap();
|
||||||
log.info("Found MDC: {}", mdc);
|
log.info("Found MDC: {}", mdc);
|
||||||
|
|
||||||
return new TestResponse(level.toString(), mdc == null ? Map.of() : mdc);
|
return new TestResponse(path, level.toString(), mdc == null ? Map.of() : mdc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +128,7 @@ public abstract class AbstractFilterTest extends AbstractTest {
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public static class TestResponse {
|
public static class TestResponse {
|
||||||
|
private String path;
|
||||||
private String level;
|
private String level;
|
||||||
private Map<String, String> context = Map.of();
|
private Map<String, String> context = Map.of();
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,6 @@ public class LogLevelServerFilterTest extends AbstractFilterTest {
|
||||||
|
|
||||||
private void assertLevel(Level expectedLevel, String name, String value) {
|
private void assertLevel(Level expectedLevel, String name, String value) {
|
||||||
var headers = value == null ? Map.<String, String>of() : Map.of(name, value);
|
var headers = value == null ? Map.<String, String>of() : Map.of(name, value);
|
||||||
assertEquals(expectedLevel.toString(), get(headers).getLevel());
|
assertEquals(expectedLevel.toString(), get("/level", headers).getLevel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ public class AuthenticationMdcFilterTest extends AbstractFilterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, String> getContext(boolean token) {
|
private Map<String, String> getContext(boolean token) {
|
||||||
return get(token
|
return get("/security", token
|
||||||
? Map.of(HttpHeaders.AUTHORIZATION, token("mySubject", claims -> claims
|
? Map.of(HttpHeaders.AUTHORIZATION, token("mySubject", claims -> claims
|
||||||
.issuer("nope")
|
.issuer("nope")
|
||||||
.claim("azp", "myAzp")
|
.claim("azp", "myAzp")
|
||||||
|
|
|
@ -55,6 +55,6 @@ public class HeaderMdcFilterTest extends AbstractFilterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertContext(Map<String, String> expectedMdcs, Map<String, String> headers) {
|
private void assertContext(Map<String, String> expectedMdcs, Map<String, String> headers) {
|
||||||
assertEquals(expectedMdcs, get(headers).getContext());
|
assertEquals(expectedMdcs, get("/header", headers).getContext());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
package io.kokuwa.micronaut.logging.http.mdc;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
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 PathMdcFilter}.
|
||||||
|
*
|
||||||
|
* @author Stephan Schnabel
|
||||||
|
*/
|
||||||
|
@DisplayName("http: mdc from path")
|
||||||
|
public class PathMdcFilterTest extends AbstractFilterTest {
|
||||||
|
|
||||||
|
@DisplayName("noop: empty configuration")
|
||||||
|
@Test
|
||||||
|
void noopEmptyConfiguration() {
|
||||||
|
assertContext(Map.of(), "/foo/bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("noop: disabled")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.enabled", value = "false")
|
||||||
|
@Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?<foo>[0-9]+)")
|
||||||
|
void noopDisabled() {
|
||||||
|
assertContext(Map.of(), "/foo/123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("noop: misconfigured")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.patterns", value = "\\A{")
|
||||||
|
void noopMisconfigured() {
|
||||||
|
assertContext(Map.of(), "/foo/123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("noop: no group")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.patterns", value = "\\/foo/[0-9]+")
|
||||||
|
void noopGroups() {
|
||||||
|
assertContext(Map.of(), "/foo/123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("mdc: mismatch")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?<foo>[0-9]+)")
|
||||||
|
void mdcMismatch() {
|
||||||
|
assertContext(Map.of(), "/nope");
|
||||||
|
assertContext(Map.of(), "/foo/abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("mdc: match with single group")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?<foo>[0-9]+)")
|
||||||
|
void mdcMatchWithSingleGroup() {
|
||||||
|
assertContext(Map.of("foo", "123"), "/foo/123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("mdc: match with single group and prefix")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.names", value = "foo")
|
||||||
|
@Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?<foo>[0-9]+)")
|
||||||
|
@Property(name = "logger.http.path.prefix", value = "path.")
|
||||||
|
void mdcMatchWithSingleGroupAndPrefix() {
|
||||||
|
assertContext(Map.of("path.foo", "123"), "/foo/123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("mdc: match with single group and misconfigured")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.names", value = "foo")
|
||||||
|
@Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?<foo>[0-9]+),\\A{")
|
||||||
|
@Property(name = "logger.http.path.prefix", value = "path.")
|
||||||
|
void mdcMatchWithSingleGroupAndMisconfigured() {
|
||||||
|
assertContext(Map.of("path.foo", "123"), "/foo/123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("mdc: match with multiple group")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.patterns", value = "/foo/(?<foo>[0-9]+)/bar/(?<bar>[0-9]+)")
|
||||||
|
void mdcMatchWithmultipleGroup() {
|
||||||
|
assertContext(Map.of("foo", "123", "bar", "456"), "/foo/123/bar/456");
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("mdc: test for documentation example")
|
||||||
|
@Test
|
||||||
|
@Property(name = "logger.http.path.patterns", value = ""
|
||||||
|
+ "\\/gateway\\/(?<gatewayId>[a-f0-9\\-]{36}),"
|
||||||
|
+ "\\/gateway\\/(?<gatewayId>[a-f0-9\\-]{36})\\/configuration\\/(?<config>[a-z]+)")
|
||||||
|
void mdcMatchExample() {
|
||||||
|
var uuid = UUID.randomUUID().toString();
|
||||||
|
assertContext(Map.of("gatewayId", uuid), "/gateway/" + uuid);
|
||||||
|
assertContext(Map.of("gatewayId", uuid, "config", "abc"), "/gateway/" + uuid + "/configuration/abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertContext(Map<String, String> expectedMdcs, String path) {
|
||||||
|
assertEquals(expectedMdcs, get(path, Map.of()).getContext());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue