From 4d65b25eac66e3cff3b743e51463c5e518f79248 Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Thu, 25 Jul 2024 17:38:35 -0300 Subject: [PATCH] Include application, http, and geoserver ows request properties in the logging MDC --- src/starters/observability/pom.xml | 34 +++- .../GeoServerDispatcherMDCConfiguration.java | 37 +++++ .../LoggingMDCAutoConfiguration.java | 93 +++++++++++ .../logging/config/MDCConfigProperties.java | 18 +++ .../logging/ows/MDCDispatcherCallback.java | 43 +++++ .../HttpRequestMdcConfigProperties.java | 43 +++++ .../logging/servlet/HttpRequestMdcFilter.java | 152 ++++++++++++++++++ .../servlet/MDCAuthenticationFilter.java | 65 ++++++++ .../logging/servlet/MDCCleaningFilter.java | 29 ++++ .../SpringEnvironmentMdcConfigProperties.java | 19 +++ .../servlet/SpringEnvironmentMdcFilter.java | 76 +++++++++ .../main/resources/META-INF/spring.factories | 2 + .../LoggingMDCAutoConfigurationTest.java | 75 +++++++++ .../GatewaySharedAuthenticationFilter.java | 1 + 14 files changed, 686 insertions(+), 1 deletion(-) create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/GeoServerDispatcherMDCConfiguration.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfiguration.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/config/MDCConfigProperties.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/ows/MDCDispatcherCallback.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcConfigProperties.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcFilter.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCAuthenticationFilter.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCCleaningFilter.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcConfigProperties.java create mode 100644 src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcFilter.java create mode 100644 src/starters/observability/src/main/resources/META-INF/spring.factories create mode 100644 src/starters/observability/src/test/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfigurationTest.java diff --git a/src/starters/observability/pom.xml b/src/starters/observability/pom.xml index 5de28acdc..0520df315 100644 --- a/src/starters/observability/pom.xml +++ b/src/starters/observability/pom.xml @@ -8,11 +8,43 @@ gs-cloud-starter-observability jar - Spring boot starter for application observability (logging, metrics, tracing) + Spring boot starter for application observability (logging, + metrics, tracing) + + org.springframework.boot + spring-boot-starter + provided + + + com.github.f4b6a3 + ulid-creator + + + + javax.servlet + javax.servlet-api + provided + true + net.logstash.logback logstash-logback-encoder + + org.geoserver + gs-main + true + + + org.springframework + spring-webmvc + true + + + org.springframework.boot + spring-boot-configuration-processor + true + diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/GeoServerDispatcherMDCConfiguration.java b/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/GeoServerDispatcherMDCConfiguration.java new file mode 100644 index 000000000..a46e058a2 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/GeoServerDispatcherMDCConfiguration.java @@ -0,0 +1,37 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.autoconfigure.observability; + +import org.geoserver.cloud.observability.logging.config.MDCConfigProperties; +import org.geoserver.cloud.observability.logging.ows.MDCDispatcherCallback; +import org.geoserver.ows.Dispatcher; +import org.geoserver.ows.DispatcherCallback; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link AutoConfiguration @AutoConfiguration} to enable logging MDC (Mapped Diagnostic Context) + * for the GeoSever {@link Dispatcher} events using a {@link DispatcherCallback} + * + * @see MDCDispatcherCallback + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ + Dispatcher.class, + // from spring-webmvc, required by Dispatcher.class + org.springframework.web.servlet.mvc.AbstractController.class +}) +@ConditionalOnWebApplication(type = Type.SERVLET) +class GeoServerDispatcherMDCConfiguration { + + @Bean + MDCDispatcherCallback mdcDispatcherCallback(MDCConfigProperties config) { + return new MDCDispatcherCallback(config); + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfiguration.java b/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfiguration.java new file mode 100644 index 000000000..dc516870c --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfiguration.java @@ -0,0 +1,93 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.autoconfigure.observability; + +import org.geoserver.cloud.observability.logging.config.MDCConfigProperties; +import org.geoserver.cloud.observability.logging.servlet.HttpRequestMdcConfigProperties; +import org.geoserver.cloud.observability.logging.servlet.HttpRequestMdcFilter; +import org.geoserver.cloud.observability.logging.servlet.MDCAuthenticationFilter; +import org.geoserver.cloud.observability.logging.servlet.MDCCleaningFilter; +import org.geoserver.cloud.observability.logging.servlet.SpringEnvironmentMdcConfigProperties; +import org.geoserver.cloud.observability.logging.servlet.SpringEnvironmentMdcFilter; +import org.geoserver.security.GeoServerSecurityFilterChainProxy; +import org.slf4j.MDC; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; + +import java.util.Optional; + +/** + * {@link AutoConfiguration @AutoConfiguration} to enable logging MDC (Mapped Diagnostic Context) + * contributions during the request life cycle + * + * @see GeoServerDispatcherMDCConfiguration + */ +@AutoConfiguration +@EnableConfigurationProperties({ + MDCConfigProperties.class, + HttpRequestMdcConfigProperties.class, + SpringEnvironmentMdcConfigProperties.class +}) +@Import(GeoServerDispatcherMDCConfiguration.class) +@ConditionalOnWebApplication(type = Type.SERVLET) +public class LoggingMDCAutoConfiguration { + + /** + * @return servlet filter to {@link MDC#clear() clear} the MDC after the servlet request is + * executed + */ + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + HttpRequestMdcFilter httpMdcFilter(HttpRequestMdcConfigProperties config) { + return new HttpRequestMdcFilter(config); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + MDCCleaningFilter mdcCleaningServletFilter() { + return new MDCCleaningFilter(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SpringEnvironmentMdcFilter springEnvironmentMdcFilter( + Environment env, + SpringEnvironmentMdcConfigProperties config, + Optional buildProperties) { + return new SpringEnvironmentMdcFilter(env, buildProperties, config); + } + + /** + * A servlet registration for {@link MDCAuthenticationFilter}, with {@link + * FilterRegistrationBean#setMatchAfter setMatchAfter(true)} to ensure it runs after {@link + * GeoServerSecurityFilterChainProxy} and hence the {@link SecurityContext} already has the + * {@link Authentication} object. + */ + @Bean + FilterRegistrationBean mdcAuthenticationPropertiesServletFilter( + MDCConfigProperties config) { + FilterRegistrationBean registration = + new FilterRegistrationBean<>(); + + var filter = new MDCAuthenticationFilter(config); + registration.setMatchAfter(true); + + registration.addUrlPatterns("/*"); + registration.setOrder(Ordered.LOWEST_PRECEDENCE); + registration.setFilter(filter); + return registration; + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/config/MDCConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/config/MDCConfigProperties.java new file mode 100644 index 000000000..7a429f9b0 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/config/MDCConfigProperties.java @@ -0,0 +1,18 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.observability.logging.config; + +import lombok.Data; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "logging.mdc.include") +public class MDCConfigProperties { + + private boolean user = true; + private boolean roles = true; + private boolean ows = true; +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/ows/MDCDispatcherCallback.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/ows/MDCDispatcherCallback.java new file mode 100644 index 000000000..b46ce7327 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/ows/MDCDispatcherCallback.java @@ -0,0 +1,43 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.observability.logging.ows; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import org.geoserver.cloud.observability.logging.config.MDCConfigProperties; +import org.geoserver.ows.AbstractDispatcherCallback; +import org.geoserver.ows.DispatcherCallback; +import org.geoserver.ows.Request; +import org.geoserver.platform.Operation; +import org.geoserver.platform.Service; +import org.slf4j.MDC; + +@RequiredArgsConstructor +public class MDCDispatcherCallback extends AbstractDispatcherCallback + implements DispatcherCallback { + + private final @NonNull MDCConfigProperties config; + + @Override + public Service serviceDispatched(Request request, Service service) { + if (config.isOws()) { + MDC.put("gs.ows.service.name", service.getId()); + MDC.put("gs.ows.service.version", String.valueOf(service.getVersion())); + if (null != request.getOutputFormat()) { + MDC.put("gs.ows.service.format", request.getOutputFormat()); + } + } + return super.serviceDispatched(request, service); + } + + @Override + public Operation operationDispatched(Request request, Operation operation) { + if (config.isOws()) { + MDC.put("gs.ows.service.operation", operation.getId()); + } + return super.operationDispatched(request, operation); + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcConfigProperties.java new file mode 100644 index 000000000..021018235 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcConfigProperties.java @@ -0,0 +1,43 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.observability.logging.servlet; + +import lombok.Data; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.regex.Pattern; + +@Data +@ConfigurationProperties(prefix = "logging.mdc.include.http") +public class HttpRequestMdcConfigProperties { + + private boolean id = true; + + /** + * The Internet Protocol (IP) address of the client or last proxy that sent the request. For + * HTTP servlets, same as the value of the CGI variable REMOTE_ADDR. + */ + private boolean remoteAddr = true; + + /** + * The fully qualified name of the client or the last proxy that sent the request. If the engine + * cannot or chooses not to resolve the hostname (to improve performance), this method returns + * the dotted-string form of the IP address. For HTTP servlets, same as the value of the CGI + * variable REMOTE_HOST. Defaults to false to avoid the possible overhead in reverse DNS + * lookups. remoteAddress should be enough in most cases. + */ + private boolean remoteHost = true; + + private boolean method = true; + private boolean url = true; + private boolean parameters = true; + private boolean queryString = true; + private boolean sessionId = true; + + private boolean cookies = true; + private boolean headers = true; + private Pattern headersPattern = Pattern.compile(".*"); +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcFilter.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcFilter.java new file mode 100644 index 000000000..0462777d1 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcFilter.java @@ -0,0 +1,152 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.observability.logging.servlet; + +import com.github.f4b6a3.ulid.UlidCreator; +import com.google.common.collect.Streams; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import org.slf4j.MDC; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +@RequiredArgsConstructor +public class HttpRequestMdcFilter extends OncePerRequestFilter { + + private final @NonNull HttpRequestMdcConfigProperties config; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + if (request instanceof HttpServletRequest req) addRequestMdcProperties(req); + } finally { + chain.doFilter(request, response); + } + } + + private void addRequestMdcProperties(HttpServletRequest req) { + HttpSession session = req.getSession(false); + + put("http.request.id", config::isId, () -> requestId(req)); + put("http.request.remote-addr", config::isRemoteAddr, req::getRemoteAddr); + put("http.request.remote-host", config::isRemoteHost, req::getRemoteHost); + + put("http.request.method", config::isMethod, req::getMethod); + put("http.request.url", config::isUrl, req::getRequestURL); + putRequestParams(req); + put("http.request.query-string", config::isQueryString, req::getQueryString); + put( + "http.request.session.id", + config::isSessionId, + () -> session == null ? null : session.getId()); + put( + "http.request.session.started", + config::isSessionId, + () -> session == null ? null : !session.isNew()); + addHeaders(req); + addCookies(req); + } + + private void putRequestParams(HttpServletRequest req) { + if (config.isParameters()) { + Streams.stream(req.getParameterNames().asIterator()) + .forEach( + name -> + put( + "http.request.parameter.%s".formatted(name), + requestParam(name, req))); + } + } + + private String requestParam(String name, HttpServletRequest req) { + String[] values = req.getParameterValues(name); + if (null == values) return null; + if (values.length == 1) return values[0]; + return null; + } + + private void addHeaders(HttpServletRequest req) { + if (config.isHeaders()) { + Streams.stream(req.getHeaderNames().asIterator()) + .filter(h -> !"cookie".equalsIgnoreCase(h)) + .filter(this::includeHeader) + .forEach(name -> putHeader(name, req)); + } + } + + private void putHeader(String name, HttpServletRequest req) { + put("http.request.header.%s".formatted(name), () -> getHeader(name, req)); + } + + private String getHeader(String name, HttpServletRequest req) { + return Streams.stream(req.getHeaders(name).asIterator()).collect(Collectors.joining(",")); + } + + private boolean includeHeader(String headerName) { + return config.getHeadersPattern().matcher(headerName).matches(); + } + + private void addCookies(HttpServletRequest req) { + if (config.isCookies()) { + Cookie[] cookies = req.getCookies(); + if (null != cookies) { + Stream.of(cookies).forEach(this::put); + } + } + } + + private void put(Cookie c) { + String key = "http.request.cookie.%s".formatted(c.getName()); + String value = MDC.get(key); + if (value == null) { + value = c.getValue(); + } else { + value = "%s;%s".formatted(value, c.getValue()); + } + MDC.put(key, value); + } + + private void put(String key, BooleanSupplier enabled, Supplier value) { + if (enabled.getAsBoolean()) { + put(key, value); + } + } + + private void put(String key, Supplier value) { + Object val = value.get(); + String svalue = val == null ? null : String.valueOf(val); + put(key, svalue); + } + + private void put(@NonNull String key, String value) { + MDC.put(key, value); + } + + /** + * @return the id provided by the {@code http.request.id} header, or a new monotonically + * increating UID if no such header is present + */ + private String requestId(HttpServletRequest req) { + return Optional.ofNullable(req.getHeader("http.request.id")) + .orElseGet(() -> UlidCreator.getMonotonicUlid().toLowerCase()); + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCAuthenticationFilter.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCAuthenticationFilter.java new file mode 100644 index 000000000..8d592c38c --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCAuthenticationFilter.java @@ -0,0 +1,65 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.observability.logging.servlet; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import org.geoserver.cloud.observability.logging.config.MDCConfigProperties; +import org.slf4j.MDC; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.io.IOException; +import java.util.stream.Collectors; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +/** + * Appends the {@code enduser.id} and {@code enduser.role} MDC properties depending on whether + * {@link MDCConfigProperties#isUser() user} and {@link MDCConfigProperties#isRoles() roles} config + * properties are enabled, respectivelly. + * + *

Note the appended MDC properties follow the OpenTelemetry + * identity attributes convention, so we can replace this component if OTel would automatically + * add them to the logs. + */ +@RequiredArgsConstructor +public class MDCAuthenticationFilter implements Filter { + + private final @NonNull MDCConfigProperties config; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + addEnduserMdcProperties(); + } finally { + chain.doFilter(request, response); + } + } + + void addEnduserMdcProperties() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + boolean authenticated = auth != null && auth.isAuthenticated(); + MDC.put("enduser.authenticated", String.valueOf(authenticated)); + if (authenticated) { + if (config.isUser()) MDC.put("enduser.id", auth.getName()); + if (config.isRoles()) MDC.put("enduser.role", roles(auth)); + } + } + + private String roles(Authentication auth) { + return auth.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCCleaningFilter.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCCleaningFilter.java new file mode 100644 index 000000000..bf159e1f4 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCCleaningFilter.java @@ -0,0 +1,29 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.observability.logging.servlet; + +import org.slf4j.MDC; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class MDCCleaningFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + chain.doFilter(request, response); + } finally { + MDC.clear(); + } + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcConfigProperties.java new file mode 100644 index 000000000..99c5396d0 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcConfigProperties.java @@ -0,0 +1,19 @@ +package org.geoserver.cloud.observability.logging.servlet; + +import lombok.Data; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@Data +@ConfigurationProperties(prefix = "logging.mdc.include.application") +public class SpringEnvironmentMdcConfigProperties { + + private boolean name = true; + private boolean version = true; + private boolean instanceId = true; + private List instanceIdProperties = + List.of("info.instance-id", "spring.application.instance_id"); + private boolean activeProfiles = true; +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcFilter.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcFilter.java new file mode 100644 index 000000000..09673fb36 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcFilter.java @@ -0,0 +1,76 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.observability.logging.servlet; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import org.slf4j.MDC; +import org.springframework.boot.info.BuildProperties; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@RequiredArgsConstructor +public class SpringEnvironmentMdcFilter extends OncePerRequestFilter { + + private final @NonNull Environment env; + private final @NonNull Optional buildProperties; + private final @NonNull SpringEnvironmentMdcConfigProperties config; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + addEnvironmentProperties(); + } finally { + chain.doFilter(request, response); + } + } + + private void addEnvironmentProperties() { + if (config.isName()) + MDC.put("application.name", env.getProperty("spring.application.name")); + + putVersion(); + putInstanceId(); + + if (config.isActiveProfiles()) + MDC.put( + "spring.profiles.active", + Stream.of(env.getActiveProfiles()).collect(Collectors.joining(","))); + } + + private void putVersion() { + if (config.isVersion()) { + buildProperties + .map(BuildProperties::getVersion) + .ifPresent(v -> MDC.put("application.version", v)); + } + } + + private void putInstanceId() { + if (!config.isInstanceId() || null == config.getInstanceIdProperties()) return; + + for (String prop : config.getInstanceIdProperties()) { + String value = env.getProperty(prop); + if (StringUtils.hasText(value)) { + MDC.put("application.instance.id", value); + return; + } + } + } +} diff --git a/src/starters/observability/src/main/resources/META-INF/spring.factories b/src/starters/observability/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..ae96425f5 --- /dev/null +++ b/src/starters/observability/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.geoserver.cloud.autoconfigure.observability.LoggingMDCAutoConfiguration \ No newline at end of file diff --git a/src/starters/observability/src/test/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfigurationTest.java b/src/starters/observability/src/test/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfigurationTest.java new file mode 100644 index 000000000..b0fa4a2dd --- /dev/null +++ b/src/starters/observability/src/test/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfigurationTest.java @@ -0,0 +1,75 @@ +/* + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ +package org.geoserver.cloud.autoconfigure.observability; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.geoserver.cloud.observability.logging.config.MDCConfigProperties; +import org.geoserver.cloud.observability.logging.ows.MDCDispatcherCallback; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +class LoggingMDCAutoConfigurationTest { + + private WebApplicationContextRunner runner = + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(LoggingMDCAutoConfiguration.class)); + + @Test + void testDefaultBeans() { + runner.run( + context -> + assertThat(context) + .hasNotFailed() + .hasSingleBean(MDCConfigProperties.class) + .hasSingleBean(MDCDispatcherCallback.class) + .hasBean("mdcCleaningServletFilter")); + } + + @Test + void testMDCConfigProperties() { + MDCConfigProperties defaults = new MDCConfigProperties(); + + runner.withPropertyValues( + "logging.mdc.include.user=%s".formatted(!defaults.isUser()), + "logging.mdc.include.roles=%s".formatted(!defaults.isRoles()), + "logging.mdc.include.ows=%s".formatted(!defaults.isOws())) + .run( + context -> + assertThat(context) + .getBean(MDCConfigProperties.class) + .hasFieldOrPropertyWithValue("user", !defaults.isUser()) + .hasFieldOrPropertyWithValue("roles", !defaults.isRoles()) + .hasFieldOrPropertyWithValue("ows", !defaults.isOws())); + } + + @Test + void conditionalOnGeoServerDispatcher() { + runner.withClassLoader(new FilteredClassLoader(org.geoserver.ows.Dispatcher.class)) + .run( + context -> + assertThat(context) + .hasNotFailed() + .doesNotHaveBean(MDCDispatcherCallback.class)); + } + + @Test + void conditionalOnServletWebApplication() { + ReactiveWebApplicationContextRunner reactiveAppRunner = + new ReactiveWebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(LoggingMDCAutoConfiguration.class)); + reactiveAppRunner.run( + context -> + assertThat(context) + .hasNotFailed() + .doesNotHaveBean(MDCConfigProperties.class) + .doesNotHaveBean(MDCDispatcherCallback.class) + .doesNotHaveBean("mdcCleaningServletFilter")); + } +} diff --git a/src/starters/security/src/main/java/org/geoserver/cloud/security/gateway/sharedauth/GatewaySharedAuthenticationFilter.java b/src/starters/security/src/main/java/org/geoserver/cloud/security/gateway/sharedauth/GatewaySharedAuthenticationFilter.java index f94a26226..4011e11ba 100644 --- a/src/starters/security/src/main/java/org/geoserver/cloud/security/gateway/sharedauth/GatewaySharedAuthenticationFilter.java +++ b/src/starters/security/src/main/java/org/geoserver/cloud/security/gateway/sharedauth/GatewaySharedAuthenticationFilter.java @@ -145,6 +145,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha private String getHeaders(HttpServletRequest req) { return Streams.stream(req.getHeaderNames().asIterator()) + .filter(h -> h.toLowerCase().startsWith("x-gsc")) .map(name -> "\t%s: %s".formatted(name, req.getHeader(name))) .collect(Collectors.joining("\n")); }