diff --git a/compose/.env b/compose/.env index 66e465b79..bb18bcbd1 100644 --- a/compose/.env +++ b/compose/.env @@ -15,7 +15,7 @@ GEOSERVER_BASE_PATH=/geoserver/cloud # logging profile, either "default" or "json-logs" #LOGGING_PROFILE=json-logs LOGGING_PROFILE=default -GEOSERVER_DEFAULT_PROFILES="${LOGGING_PROFILE},acl,logging_debug_events" +GEOSERVER_DEFAULT_PROFILES="${LOGGING_PROFILE},acl" GATEWAY_DEFAULT_PROFILES=${LOGGING_PROFILE} DISCOVERY_SERVER_DEFAULT_PROFILES=${LOGGING_PROFILE} diff --git a/config b/config index 85a5df9ff..4a727b965 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 85a5df9ffce34110fa24ca246842a1cca0a28dfd +Subproject commit 4a727b96541b7cc46d423cebed17423c5737cc2a diff --git a/src/starters/observability/pom.xml b/src/starters/observability/pom.xml index 0520df315..7eae7fa7d 100644 --- a/src/starters/observability/pom.xml +++ b/src/starters/observability/pom.xml @@ -20,7 +20,6 @@ com.github.f4b6a3 ulid-creator - javax.servlet javax.servlet-api diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/logging/accesslog/AccessLogServletAutoConfiguration.java b/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/logging/accesslog/AccessLogServletAutoConfiguration.java new file mode 100644 index 000000000..359114628 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/logging/accesslog/AccessLogServletAutoConfiguration.java @@ -0,0 +1,26 @@ +/* + * (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.logging.accesslog; + +import org.geoserver.cloud.logging.accesslog.AccessLogFilterConfig; +import org.geoserver.cloud.logging.accesslog.AccessLogServletFilter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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.context.annotation.Bean; + +@AutoConfiguration +@ConditionalOnProperty(name = AccessLogFilterConfig.ENABLED_KEY, havingValue = "true", matchIfMissing = false) +@EnableConfigurationProperties(AccessLogFilterConfig.class) +@ConditionalOnWebApplication(type = Type.SERVLET) +public class AccessLogServletAutoConfiguration { + + @Bean + AccessLogServletFilter accessLogFilter(AccessLogFilterConfig conf) { + return new AccessLogServletFilter(conf); + } +} 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/logging/mdc/GeoServerDispatcherMDCConfiguration.java similarity index 75% rename from src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/GeoServerDispatcherMDCConfiguration.java rename to src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/logging/mdc/GeoServerDispatcherMDCConfiguration.java index a46e058a2..669d14e12 100644 --- 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/logging/mdc/GeoServerDispatcherMDCConfiguration.java @@ -2,10 +2,10 @@ * (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; +package org.geoserver.cloud.autoconfigure.logging.mdc; -import org.geoserver.cloud.observability.logging.config.MDCConfigProperties; -import org.geoserver.cloud.observability.logging.ows.MDCDispatcherCallback; +import org.geoserver.cloud.logging.mdc.config.MDCConfigProperties; +import org.geoserver.cloud.logging.mdc.ows.OWSMdcDispatcherCallback; import org.geoserver.ows.Dispatcher; import org.geoserver.ows.DispatcherCallback; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -19,7 +19,7 @@ * {@link AutoConfiguration @AutoConfiguration} to enable logging MDC (Mapped Diagnostic Context) * for the GeoSever {@link Dispatcher} events using a {@link DispatcherCallback} * - * @see MDCDispatcherCallback + * @see OWSMdcDispatcherCallback */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ @@ -31,7 +31,7 @@ class GeoServerDispatcherMDCConfiguration { @Bean - MDCDispatcherCallback mdcDispatcherCallback(MDCConfigProperties config) { - return new MDCDispatcherCallback(config); + OWSMdcDispatcherCallback mdcDispatcherCallback(MDCConfigProperties config) { + return new OWSMdcDispatcherCallback(config.getGeoserver().getOws()); } } 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/logging/mdc/LoggingMDCServletAutoConfiguration.java similarity index 68% rename from src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfiguration.java rename to src/starters/observability/src/main/java/org/geoserver/cloud/autoconfigure/logging/mdc/LoggingMDCServletAutoConfiguration.java index 26c717a21..c107971d4 100644 --- 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/logging/mdc/LoggingMDCServletAutoConfiguration.java @@ -2,16 +2,14 @@ * (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; +package org.geoserver.cloud.autoconfigure.logging.mdc; import java.util.Optional; -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.cloud.logging.mdc.config.MDCConfigProperties; +import org.geoserver.cloud.logging.mdc.servlet.HttpRequestMdcFilter; +import org.geoserver.cloud.logging.mdc.servlet.MDCAuthenticationFilter; +import org.geoserver.cloud.logging.mdc.servlet.MDCCleaningFilter; +import org.geoserver.cloud.logging.mdc.servlet.SpringEnvironmentMdcFilter; import org.geoserver.security.GeoServerSecurityFilterChainProxy; import org.slf4j.MDC; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -36,36 +34,31 @@ * @see GeoServerDispatcherMDCConfiguration */ @AutoConfiguration -@EnableConfigurationProperties({ - MDCConfigProperties.class, - HttpRequestMdcConfigProperties.class, - SpringEnvironmentMdcConfigProperties.class -}) +@EnableConfigurationProperties({MDCConfigProperties.class}) @Import(GeoServerDispatcherMDCConfiguration.class) @ConditionalOnWebApplication(type = Type.SERVLET) -public class LoggingMDCAutoConfiguration { +public class LoggingMDCServletAutoConfiguration { + + @Bean + MDCCleaningFilter mdcCleaningServletFilter() { + return new MDCCleaningFilter(); + } /** * @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(); + @Order(Ordered.HIGHEST_PRECEDENCE + 2) + HttpRequestMdcFilter httpMdcFilter(MDCConfigProperties config) { + return new HttpRequestMdcFilter(config.getHttp()); } @Bean - @Order(Ordered.HIGHEST_PRECEDENCE) + @Order(Ordered.HIGHEST_PRECEDENCE + 2) SpringEnvironmentMdcFilter springEnvironmentMdcFilter( - Environment env, SpringEnvironmentMdcConfigProperties config, Optional buildProperties) { - return new SpringEnvironmentMdcFilter(env, buildProperties, config); + Environment env, MDCConfigProperties config, Optional buildProperties) { + return new SpringEnvironmentMdcFilter(env, buildProperties, config.getApplication()); } /** @@ -80,7 +73,7 @@ FilterRegistrationBean mdcAuthenticationPropertiesServl MDCConfigProperties config) { FilterRegistrationBean registration = new FilterRegistrationBean<>(); - var filter = new MDCAuthenticationFilter(config); + var filter = new MDCAuthenticationFilter(config.getUser()); registration.setMatchAfter(true); registration.addUrlPatterns("/*"); diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/logging/accesslog/AccessLogFilterConfig.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/accesslog/AccessLogFilterConfig.java new file mode 100644 index 000000000..acb10a6d7 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/accesslog/AccessLogFilterConfig.java @@ -0,0 +1,90 @@ +/* + * (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.logging.accesslog; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration to set white/black list over the request URL to determine if + * the access log filter will log an entry for it. + */ +@Data +@ConfigurationProperties(prefix = "logging.accesslog") +@Slf4j(topic = "org.geoserver.cloud.accesslog") +public class AccessLogFilterConfig { + + public static final String ENABLED_KEY = "logging.accesslog.enabled"; + + /** + * A list of java regular expressions applied to the request URL for logging at + * trace level + */ + List trace = new ArrayList<>(); + + /** + * A list of java regular expressions applied to the request URL for logging at + * debug level + */ + List debug = new ArrayList<>(); + + /** + * A list of java regular expressions applied to the request URL for logging at + * info level + */ + List info = new ArrayList<>(); + + private enum Level { + OFF { + @Override + void log(String message, Object... args) { + // no-op + } + }, + TRACE { + @Override + void log(String message, Object... args) { + log.trace(message, args); + } + }, + DEBUG { + @Override + void log(String message, Object... args) { + log.debug(message, args); + } + }, + INFO { + @Override + void log(String message, Object... args) { + log.info(message, args); + } + }; + + abstract void log(String message, Object... args); + } + + public void log(String method, int statusCode, String uri) { + Level level = getLogLevel(uri); + level.log("{} {} {} ", method, statusCode, uri); + } + + Level getLogLevel(String uri) { + if (log.isInfoEnabled() && matches(uri, info)) return Level.INFO; + if (log.isDebugEnabled() && matches(uri, debug)) return Level.INFO; + if (log.isTraceEnabled() && matches(uri, trace)) return Level.INFO; + + return Level.OFF; + } + + private boolean matches(String url, List patterns) { + return (patterns == null || patterns.isEmpty()) + ? false + : patterns.stream().anyMatch(pattern -> pattern.matcher(url).matches()); + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/logging/accesslog/AccessLogServletFilter.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/accesslog/AccessLogServletFilter.java new file mode 100644 index 000000000..4dcdf26b4 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/accesslog/AccessLogServletFilter.java @@ -0,0 +1,41 @@ +/* + * (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.logging.accesslog; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.NonNull; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import org.springframework.web.filter.OncePerRequestFilter; + +/** Similar to {@link CommonsRequestLoggingFilter} but uses slf4j */ +@Order(Ordered.HIGHEST_PRECEDENCE + 3) +public class AccessLogServletFilter extends OncePerRequestFilter { + + private final @NonNull AccessLogFilterConfig config; + + public AccessLogServletFilter(@NonNull AccessLogFilterConfig conf) { + this.config = conf; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + try { + filterChain.doFilter(request, response); + } finally { + String uri = request.getRequestURI(); + String method = request.getMethod(); + int statusCode = response.getStatus(); + config.log(method, statusCode, uri); + } + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/AuthenticationMdcConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/AuthenticationMdcConfigProperties.java new file mode 100644 index 000000000..3184c5e59 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/AuthenticationMdcConfigProperties.java @@ -0,0 +1,19 @@ +/* + * (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.logging.mdc.config; + +import lombok.Data; + +@Data +public class AuthenticationMdcConfigProperties { + + /** Whether to append the enduser.id MDC property from the Authentication name */ + private boolean id = false; + + /** + * Whether to append the enduser.roles MDC property from the Authentication granted authorities + */ + private boolean roles = false; +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/GeoServerMdcConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/GeoServerMdcConfigProperties.java new file mode 100644 index 000000000..77e7ab1f9 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/GeoServerMdcConfigProperties.java @@ -0,0 +1,38 @@ +/* + * (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.logging.mdc.config; + +import lombok.Data; + +@Data +public class GeoServerMdcConfigProperties { + + private OWSMdcConfigProperties ows = new OWSMdcConfigProperties(); + + /** Configuration properties to contribute GeoServer OWS request properties to the MDC */ + @Data + public static class OWSMdcConfigProperties { + /** + * Whether to append the gs.ows.service.name MDC property from the OWS dispatched request + */ + private boolean serviceName = true; + + /** + * Whether to append the gs.ows.service.version MDC property from the OWS dispatched request + */ + private boolean serviceVersion = true; + + /** + * Whether to append the gs.ows.service.format MDC property from the OWS dispatched request + */ + private boolean serviceFormat = true; + + /** + * Whether to append the gs.ows.service.operation MDC property from the OWS dispatched + * request + */ + private boolean operationName = true; + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/HttpRequestMdcConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/HttpRequestMdcConfigProperties.java new file mode 100644 index 000000000..6137ea9d4 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/HttpRequestMdcConfigProperties.java @@ -0,0 +1,229 @@ +/* + * (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.logging.mdc.config; + +import com.github.f4b6a3.ulid.UlidCreator; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import lombok.Data; +import lombok.NonNull; +import org.slf4j.MDC; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.util.MultiValueMap; + +/** Contributes HTTP request properties to MDC attributes */ +@Data +public class HttpRequestMdcConfigProperties { + + public static final String REQUEST_ID_HEADER = "http.request.id"; + + /** + * Whether to append the http.request.id MDC property. The value is the id provided by the + * http.request.id header, or a new monotonically increating UID if no such header is present. + */ + private boolean id = true; + + /** + * Whether to append the http.request.remote-addr MDC property, interpreted as 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 = false; + + /** + * Whether to append the http.request.remote-host MDC property, interpreted as 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 = false; + + /** Whether to append the http.request.method MDC property */ + private boolean method = true; + + /** Whether to append the http.request.url MDC property, without the query string */ + private boolean url = true; + + /** + * Whether to append one http.request.parameter.[name] MDC property from each request parameter + */ + private boolean parameters = false; + + /** + * Whether to append the http.request.query-string MDC property from the HTTP request query + * string + */ + private boolean queryString = false; + + /** + * Whether to append the http.request.session.is MDC property if there's an HttpSession + * associated to the request + */ + private boolean sessionId = false; + + /** Whether to append one http.request.cookie.[name] MDC property from each request cookie */ + private boolean cookies = false; + + /** + * Whether to append one http.request.header.[name] MDC property from each HTTP request header + * whose name matches the headers-pattern + */ + private boolean headers = false; + + /** + * Java regular expression indicating which request header names to include when + * logging.mdc.include.http.headers=true. Defaults to include all headers with the pattern '.*' + */ + private Pattern headersPattern = Pattern.compile(".*"); + + public HttpRequestMdcConfigProperties headers(Supplier headers) { + if (isHeaders()) { + HttpHeaders httpHeaders = headers.get(); + httpHeaders.forEach(this::putHeader); + } + return this; + } + + public HttpRequestMdcConfigProperties cookies(Supplier> cookies) { + if (isCookies()) { + cookies.get().values().forEach(this::putCookie); + } + return this; + } + + private void putCookie(List cookies) { + cookies.forEach(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 boolean includeHeader(String headerName) { + if ("cookie".equalsIgnoreCase(headerName)) return false; + return getHeadersPattern().matcher(headerName).matches(); + } + + private void putHeader(String name, List values) { + if (includeHeader(name)) { + put("http.request.header.%s".formatted(name), () -> values.stream().collect(Collectors.joining(","))); + } + } + + public HttpRequestMdcConfigProperties id(Supplier headers) { + put(REQUEST_ID_HEADER, this::isId, () -> findOrCreateRequestId(headers)); + return this; + } + + public HttpRequestMdcConfigProperties method(Supplier method) { + put("http.request.method", this::isMethod, method); + return this; + } + + public HttpRequestMdcConfigProperties url(Supplier url) { + put("http.request.url", this::isUrl, url); + return this; + } + + public HttpRequestMdcConfigProperties queryString(Supplier getQueryString) { + put("http.request.query-string", this::isQueryString, getQueryString); + return this; + } + + public HttpRequestMdcConfigProperties parameters(Supplier> parameters) { + if (isParameters()) { + Map> params = parameters.get(); + params.forEach((k, v) -> put("http.request.parameter.%s".formatted(k), values(v))); + } + return this; + } + + private Supplier values(List v) { + return () -> null == v ? "" : v.stream().collect(Collectors.joining(",")); + } + + public HttpRequestMdcConfigProperties sessionId(Supplier sessionId) { + put("http.request.session.id", this::isSessionId, sessionId); + return this; + } + + public HttpRequestMdcConfigProperties remoteAddr(InetSocketAddress remoteAddr) { + return remoteAddr(remoteAddr::toString); + } + + public HttpRequestMdcConfigProperties remoteAddr(Supplier remoteAddr) { + put("http.request.remote-addr", this::isRemoteAddr, remoteAddr); + return this; + } + + public HttpRequestMdcConfigProperties remoteHost(InetSocketAddress remoteHost) { + return remoteAddr(remoteHost::toString); + } + + public HttpRequestMdcConfigProperties remoteHost(Supplier remoteHost) { + put("http.request.remote-host", this::isRemoteAddr, remoteHost); + return this; + } + + 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 traceId} header, {@code http.request.id} header, or a + * new monotonically increating UID if no such header is present + */ + public static String findOrCreateRequestId(Supplier headers) { + return findRequestId(headers).orElseGet(() -> newRequestId()); + } + + /** + * @return a new monotonically increating UID + */ + public static String newRequestId() { + return UlidCreator.getMonotonicUlid().toLowerCase(); + } + + /** + * Obtains the request id, if present, fromt the {@code trace-id}, {@code http.request.id}, or + * {@code x-request-id} request headers. + */ + public static Optional findRequestId(Supplier headers) { + HttpHeaders httpHeaders = headers.get(); + return header("trace-id", httpHeaders) + .or(() -> header(REQUEST_ID_HEADER, httpHeaders)) + .or(() -> header("X-Request-ID", httpHeaders)); + } + + private static Optional header(String name, HttpHeaders headers) { + return Optional.ofNullable(headers.get(name)).filter(l -> !l.isEmpty()).map(l -> l.get(0)); + } +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/MDCConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/MDCConfigProperties.java new file mode 100644 index 000000000..be8b4a0f4 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/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.logging.mdc.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "logging.mdc.include") +public class MDCConfigProperties { + + private SpringEnvironmentMdcConfigProperties application = new SpringEnvironmentMdcConfigProperties(); + private HttpRequestMdcConfigProperties http = new HttpRequestMdcConfigProperties(); + private AuthenticationMdcConfigProperties user = new AuthenticationMdcConfigProperties(); + private GeoServerMdcConfigProperties geoserver = new GeoServerMdcConfigProperties(); +} diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/SpringEnvironmentMdcConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/SpringEnvironmentMdcConfigProperties.java new file mode 100644 index 000000000..ab5413280 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/config/SpringEnvironmentMdcConfigProperties.java @@ -0,0 +1,55 @@ +package org.geoserver.cloud.logging.mdc.config; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.Data; +import org.slf4j.MDC; +import org.springframework.boot.info.BuildProperties; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +@Data +public class SpringEnvironmentMdcConfigProperties { + + private boolean name = true; + private boolean version = false; + private boolean instanceId = false; + + /** + * Application environment property names where to extract the instance-id from. Defaults to + * [info.instance-id, spring.application.instance_id] + */ + private List instanceIdProperties = List.of("info.instance-id", "spring.application.instance_id"); + + private boolean activeProfiles = false; + + public void addEnvironmentProperties(Environment env, Optional buildProperties) { + if (isName()) MDC.put("application.name", env.getProperty("spring.application.name")); + + putVersion(buildProperties); + putInstanceId(env); + + if (isActiveProfiles()) + MDC.put("spring.profiles.active", Stream.of(env.getActiveProfiles()).collect(Collectors.joining(","))); + } + + private void putVersion(Optional buildProperties) { + if (isVersion()) { + buildProperties.map(BuildProperties::getVersion).ifPresent(v -> MDC.put("application.version", v)); + } + } + + private void putInstanceId(Environment env) { + if (!isInstanceId() || null == getInstanceIdProperties()) return; + + for (String prop : 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/java/org/geoserver/cloud/observability/logging/ows/MDCDispatcherCallback.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/ows/OWSMdcDispatcherCallback.java similarity index 56% rename from src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/ows/MDCDispatcherCallback.java rename to src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/ows/OWSMdcDispatcherCallback.java index 182c75f89..bc6a3461b 100644 --- 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/logging/mdc/ows/OWSMdcDispatcherCallback.java @@ -2,11 +2,11 @@ * (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; +package org.geoserver.cloud.logging.mdc.ows; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.geoserver.cloud.observability.logging.config.MDCConfigProperties; +import org.geoserver.cloud.logging.mdc.config.GeoServerMdcConfigProperties; import org.geoserver.ows.AbstractDispatcherCallback; import org.geoserver.ows.DispatcherCallback; import org.geoserver.ows.Request; @@ -15,25 +15,26 @@ import org.slf4j.MDC; @RequiredArgsConstructor -public class MDCDispatcherCallback extends AbstractDispatcherCallback implements DispatcherCallback { +public class OWSMdcDispatcherCallback extends AbstractDispatcherCallback implements DispatcherCallback { - private final @NonNull MDCConfigProperties config; + private final @NonNull GeoServerMdcConfigProperties.OWSMdcConfigProperties 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()); - } + if (config.isServiceName()) MDC.put("gs.ows.service.name", service.getId()); + + if (config.isServiceVersion()) MDC.put("gs.ows.service.version", String.valueOf(service.getVersion())); + + if (config.isServiceFormat() && 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()) { + if (config.isOperationName()) { 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/logging/mdc/servlet/HttpRequestMdcFilter.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/servlet/HttpRequestMdcFilter.java new file mode 100644 index 000000000..2228cac81 --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/servlet/HttpRequestMdcFilter.java @@ -0,0 +1,106 @@ +/* + * (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.logging.mdc.servlet; + +import com.google.common.base.Suppliers; +import com.google.common.collect.Streams; +import java.io.IOException; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +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; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.geoserver.cloud.logging.mdc.config.HttpRequestMdcConfigProperties; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.filter.OncePerRequestFilter; + +@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) { + Supplier headers = headers(req); + config.id(headers) + .remoteAddr(req::getRemoteAddr) + .remoteHost(req::getRemoteHost) + .method(req::getMethod) + .url(req::getRequestURI) + .queryString(req::getQueryString) + .parameters(parameters(req)) + .sessionId(sessionId(req)) + .headers(headers) + .cookies(cookies(req)); + } + + Supplier> parameters(HttpServletRequest req) { + return () -> { + var map = new LinkedMultiValueMap(); + Map params = req.getParameterMap(); + params.forEach((k, v) -> map.put(k, v == null ? null : Arrays.asList(v))); + return map; + }; + } + + private Supplier> cookies(HttpServletRequest req) { + return () -> { + Cookie[] cookies = req.getCookies(); + var map = new LinkedMultiValueMap(); + if (null != cookies && cookies.length > 0) { + for (Cookie c : cookies) { + map.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); + } + } + return map; + }; + } + + private Supplier sessionId(HttpServletRequest req) { + return () -> Optional.ofNullable(req.getSession(false)) + .map(HttpSession::getId) + .orElse(null); + } + + private Supplier headers(HttpServletRequest req) { + return Suppliers.memoize(buildHeaders(req)); + } + + private com.google.common.base.Supplier buildHeaders(HttpServletRequest req) { + return () -> { + HttpHeaders headers = new HttpHeaders(); + Streams.stream(req.getHeaderNames().asIterator()) + .forEach(name -> headers.put(name, headerValue(name, req))); + return headers; + }; + } + + private List headerValue(String name, HttpServletRequest req) { + Enumeration values = req.getHeaders(name); + if (null == values) return List.of(); + return Streams.stream(values.asIterator()).toList(); + } +} 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/logging/mdc/servlet/MDCAuthenticationFilter.java similarity index 76% rename from src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCAuthenticationFilter.java rename to src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/servlet/MDCAuthenticationFilter.java index a71b589dc..aa09ae8d6 100644 --- 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/logging/mdc/servlet/MDCAuthenticationFilter.java @@ -2,7 +2,7 @@ * (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; +package org.geoserver.cloud.logging.mdc.servlet; import java.io.IOException; import java.util.stream.Collectors; @@ -13,8 +13,10 @@ import javax.servlet.ServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.geoserver.cloud.observability.logging.config.MDCConfigProperties; +import org.geoserver.cloud.logging.mdc.config.AuthenticationMdcConfigProperties; +import org.geoserver.cloud.logging.mdc.config.MDCConfigProperties; import org.slf4j.MDC; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -32,7 +34,7 @@ @RequiredArgsConstructor public class MDCAuthenticationFilter implements Filter { - private final @NonNull MDCConfigProperties config; + private final @NonNull AuthenticationMdcConfigProperties config; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) @@ -46,10 +48,15 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha void addEnduserMdcProperties() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - boolean authenticated = auth != null && auth.isAuthenticated(); + boolean authenticated; + if (auth == null || auth instanceof AnonymousAuthenticationToken) { + authenticated = false; + } else { + authenticated = auth.isAuthenticated(); + } MDC.put("enduser.authenticated", String.valueOf(authenticated)); if (authenticated) { - if (config.isUser()) MDC.put("enduser.id", auth.getName()); + if (config.isId()) MDC.put("enduser.id", auth.getName()); if (config.isRoles()) MDC.put("enduser.role", roles(auth)); } } 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/logging/mdc/servlet/MDCCleaningFilter.java similarity index 82% rename from src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/MDCCleaningFilter.java rename to src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/servlet/MDCCleaningFilter.java index 08799717b..1a980a321 100644 --- 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/logging/mdc/servlet/MDCCleaningFilter.java @@ -2,7 +2,7 @@ * (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; +package org.geoserver.cloud.logging.mdc.servlet; import java.io.IOException; import javax.servlet.FilterChain; @@ -10,8 +10,11 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.web.filter.OncePerRequestFilter; +@Order(Ordered.HIGHEST_PRECEDENCE) public class MDCCleaningFilter extends OncePerRequestFilter { @Override diff --git a/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/servlet/SpringEnvironmentMdcFilter.java b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/servlet/SpringEnvironmentMdcFilter.java new file mode 100644 index 000000000..1237c308b --- /dev/null +++ b/src/starters/observability/src/main/java/org/geoserver/cloud/logging/mdc/servlet/SpringEnvironmentMdcFilter.java @@ -0,0 +1,36 @@ +/* + * (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.logging.mdc.servlet; + +import java.io.IOException; +import java.util.Optional; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.geoserver.cloud.logging.mdc.config.SpringEnvironmentMdcConfigProperties; +import org.springframework.boot.info.BuildProperties; +import org.springframework.core.env.Environment; +import org.springframework.web.filter.OncePerRequestFilter; + +@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 { + config.addEnvironmentProperties(env, buildProperties); + } finally { + chain.doFilter(request, response); + } + } +} 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 deleted file mode 100644 index aa37b9e20..000000000 --- a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/config/MDCConfigProperties.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * (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/servlet/HttpRequestMdcConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcConfigProperties.java deleted file mode 100644 index 273c9b22a..000000000 --- a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcConfigProperties.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * (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 java.util.regex.Pattern; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@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 deleted file mode 100644 index cf912efe3..000000000 --- a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/HttpRequestMdcFilter.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * (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 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; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import org.slf4j.MDC; -import org.springframework.web.filter.OncePerRequestFilter; - -@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/SpringEnvironmentMdcConfigProperties.java b/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcConfigProperties.java deleted file mode 100644 index 24168322c..000000000 --- a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcConfigProperties.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.geoserver.cloud.observability.logging.servlet; - -import java.util.List; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@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 deleted file mode 100644 index f64d01e34..000000000 --- a/src/starters/observability/src/main/java/org/geoserver/cloud/observability/logging/servlet/SpringEnvironmentMdcFilter.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * (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 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; -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; - -@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 index ae96425f5..fc4041544 100644 --- a/src/starters/observability/src/main/resources/META-INF/spring.factories +++ b/src/starters/observability/src/main/resources/META-INF/spring.factories @@ -1,2 +1,3 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.geoserver.cloud.autoconfigure.observability.LoggingMDCAutoConfiguration \ No newline at end of file +org.geoserver.cloud.autoconfigure.logging.mdc.LoggingMDCServletAutoConfiguration,\ +org.geoserver.cloud.autoconfigure.logging.accesslog.AccessLogServletAutoConfiguration \ 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 deleted file mode 100644 index 0aa595fed..000000000 --- a/src/starters/observability/src/test/java/org/geoserver/cloud/autoconfigure/observability/LoggingMDCAutoConfigurationTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * (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.geoserver.cloud.observability.logging.servlet.HttpRequestMdcFilter; -import org.geoserver.cloud.observability.logging.servlet.MDCCleaningFilter; -import org.geoserver.cloud.observability.logging.servlet.SpringEnvironmentMdcFilter; -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; -import org.springframework.security.core.Authentication; - -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) - .hasSingleBean(MDCCleaningFilter.class) - .hasSingleBean(HttpRequestMdcFilter.class) - .hasSingleBean(SpringEnvironmentMdcFilter.class) - .hasBean("mdcAuthenticationPropertiesServletFilter")); - } - - @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")); - } - - @Test - void authenticationFilterConditionalOnAuthenticationClass() { - runner.withClassLoader(new FilteredClassLoader(Authentication.class)) - .run(context -> - assertThat(context).hasNotFailed().doesNotHaveBean("mdcAuthenticationPropertiesServletFilter")); - } -} diff --git a/src/starters/observability/src/test/java/org/geoserver/cloud/autoconfigure/observability/servlet/LoggingMDCAutoConfigurationTest.java b/src/starters/observability/src/test/java/org/geoserver/cloud/autoconfigure/observability/servlet/LoggingMDCAutoConfigurationTest.java new file mode 100644 index 000000000..82785e130 --- /dev/null +++ b/src/starters/observability/src/test/java/org/geoserver/cloud/autoconfigure/observability/servlet/LoggingMDCAutoConfigurationTest.java @@ -0,0 +1,122 @@ +/* + * (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.servlet; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.geoserver.cloud.autoconfigure.logging.mdc.LoggingMDCServletAutoConfiguration; +import org.geoserver.cloud.logging.mdc.config.MDCConfigProperties; +import org.geoserver.cloud.logging.mdc.ows.OWSMdcDispatcherCallback; +import org.geoserver.cloud.logging.mdc.servlet.HttpRequestMdcFilter; +import org.geoserver.cloud.logging.mdc.servlet.MDCCleaningFilter; +import org.geoserver.cloud.logging.mdc.servlet.SpringEnvironmentMdcFilter; +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; +import org.springframework.security.core.Authentication; + +class LoggingMDCAutoConfigurationTest { + + private WebApplicationContextRunner runner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(LoggingMDCServletAutoConfiguration.class)); + + @Test + void testDefaultBeans() { + runner.run(context -> assertThat(context) + .hasNotFailed() + .hasSingleBean(MDCConfigProperties.class) + .hasSingleBean(OWSMdcDispatcherCallback.class) + .hasSingleBean(MDCCleaningFilter.class) + .hasSingleBean(HttpRequestMdcFilter.class) + .hasSingleBean(SpringEnvironmentMdcFilter.class) + .hasBean("mdcAuthenticationPropertiesServletFilter")); + } + + @Test + void testDefaultMDCConfigProperties() { + runner.run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(MDCConfigProperties.class); + MDCConfigProperties defaults = context.getBean(MDCConfigProperties.class); + + assertThat(defaults.getUser()) + .hasFieldOrPropertyWithValue("id", false) + .hasFieldOrPropertyWithValue("roles", false); + + assertThat(defaults.getHttp()) + .hasFieldOrPropertyWithValue("id", true) + .hasFieldOrPropertyWithValue("remoteAddr", false) + .hasFieldOrPropertyWithValue("remoteHost", false) + .hasFieldOrPropertyWithValue("method", true) + .hasFieldOrPropertyWithValue("url", true) + .hasFieldOrPropertyWithValue("parameters", false) + .hasFieldOrPropertyWithValue("queryString", false) + .hasFieldOrPropertyWithValue("sessionId", false) + .hasFieldOrPropertyWithValue("cookies", false) + .hasFieldOrPropertyWithValue("headers", false); + + assertThat(defaults.getHttp().getHeadersPattern().pattern()).isEqualTo(".*"); + + assertThat(defaults.getApplication()) + .hasFieldOrPropertyWithValue("name", true) + .hasFieldOrPropertyWithValue("version", false) + .hasFieldOrPropertyWithValue("instanceId", false) + .hasFieldOrPropertyWithValue("activeProfiles", false) + .hasFieldOrPropertyWithValue( + "instanceIdProperties", List.of("info.instance-id", "spring.application.instance_id")); + + assertThat(defaults.getGeoserver()) + .hasFieldOrPropertyWithValue("ows.serviceName", true) + .hasFieldOrPropertyWithValue("ows.serviceVersion", true) + .hasFieldOrPropertyWithValue("ows.serviceFormat", true) + .hasFieldOrPropertyWithValue("ows.operationName", true); + }); + } + + @Test + void testMDCConfigProperties() { + runner.withPropertyValues( + "logging.mdc.include.user.id=true", + "logging.mdc.include.user.roles=true", + "logging.mdc.include.application.version=true", + "logging.mdc.include.application.instance-id=true", + "logging.mdc.include.http.headers=true", + "logging.mdc.include.geoserver.ows.service-name=false") + .run(context -> assertThat(context) + .getBean(MDCConfigProperties.class) + .hasFieldOrPropertyWithValue("user.id", true) + .hasFieldOrPropertyWithValue("user.roles", true) + .hasFieldOrPropertyWithValue("application.version", true) + .hasFieldOrPropertyWithValue("application.instanceId", true) + .hasFieldOrPropertyWithValue("http.headers", true) + .hasFieldOrPropertyWithValue("geoserver.ows.serviceName", false)); + } + + @Test + void conditionalOnGeoServerDispatcher() { + runner.withClassLoader(new FilteredClassLoader(org.geoserver.ows.Dispatcher.class)) + .run(context -> assertThat(context).hasNotFailed().doesNotHaveBean(OWSMdcDispatcherCallback.class)); + } + + @Test + void conditionalOnServletWebApplication() { + ReactiveWebApplicationContextRunner reactiveAppRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(LoggingMDCServletAutoConfiguration.class)); + reactiveAppRunner.run(context -> assertThat(context) + .hasNotFailed() + .doesNotHaveBean(MDCConfigProperties.class) + .doesNotHaveBean(OWSMdcDispatcherCallback.class) + .doesNotHaveBean("mdcCleaningServletFilter")); + } + + @Test + void authenticationFilterConditionalOnAuthenticationClass() { + runner.withClassLoader(new FilteredClassLoader(Authentication.class)) + .run(context -> + assertThat(context).hasNotFailed().doesNotHaveBean("mdcAuthenticationPropertiesServletFilter")); + } +} diff --git a/src/starters/observability/src/test/resources/application.yml b/src/starters/observability/src/test/resources/application.yml new file mode 100644 index 000000000..4ee52f2e8 --- /dev/null +++ b/src/starters/observability/src/test/resources/application.yml @@ -0,0 +1,29 @@ +logging: + mdc: + include: + user: + id: false + roles: false + application: + name: true + version: true + instance-id: true + active-profiles: true + http: + id: true + method: true + url: true + query-string: false + parameters: false + headers: false + headers-pattern: ".*" + cookies: false + remote-addr: false + remote-host: false + session-id: false + geoserver: + ows: + service-name: true + service-version: true + service-format: true + operation-name: true \ No newline at end of file diff --git a/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/DataDirectoryTempSupport.java b/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/DataDirectoryTempSupport.java new file mode 100644 index 000000000..c4df5ca0c --- /dev/null +++ b/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/DataDirectoryTempSupport.java @@ -0,0 +1,23 @@ +/* + * (c) 2020 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.servlet; + +import java.nio.file.Path; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +public class DataDirectoryTempSupport { + + @TempDir + protected static Path tempDataDir; + + @DynamicPropertySource + static void setDynamicProperties(DynamicPropertyRegistry registry) { + registry.add( + "geoserver.backend.data-directory.location", + () -> tempDataDir.toAbsolutePath().toString()); + } +} diff --git a/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextConditionalFiltersTest.java b/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextConditionalFiltersTest.java index 32a277d0a..581abb05c 100644 --- a/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextConditionalFiltersTest.java +++ b/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextConditionalFiltersTest.java @@ -29,12 +29,11 @@ @EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class}) @TestPropertySource( properties = { - "reactive.feign.loadbalancer.enabled=false", "geoserver.servlet.filter.session-debug.enabled=false", "geoserver.servlet.filter.flush-safe.enabled=false" }) @ActiveProfiles("test") -class ServletContextConditionalFiltersTest { +class ServletContextConditionalFiltersTest extends DataDirectoryTempSupport { private @Autowired ApplicationContext context; diff --git a/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextDisabledSmokeTest.java b/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextDisabledSmokeTest.java index f8b8c8975..3d6286c55 100644 --- a/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextDisabledSmokeTest.java +++ b/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextDisabledSmokeTest.java @@ -29,10 +29,10 @@ */ @SpringBootTest( classes = TestConfiguration.class, - properties = {"reactive.feign.loadbalancer.enabled=false", "geoserver.servlet.enabled=false"}) + properties = {"geoserver.servlet.enabled=false"}) @EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class) @ActiveProfiles("test") -class ServletContextDisabledSmokeTest { +class ServletContextDisabledSmokeTest extends DataDirectoryTempSupport { private @Autowired ApplicationContext context; diff --git a/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextEnabledSmokeTest.java b/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextEnabledSmokeTest.java index adb10edee..2a33d5cfe 100644 --- a/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextEnabledSmokeTest.java +++ b/src/starters/webmvc/src/test/java/org/geoserver/cloud/autoconfigure/servlet/ServletContextEnabledSmokeTest.java @@ -23,10 +23,10 @@ import org.springframework.web.context.request.RequestContextListener; /** Smoke test to load the servlet context beans with auto-configuration enabled */ -@SpringBootTest(classes = TestConfiguration.class, properties = "reactive.feign.loadbalancer.enabled=false") +@SpringBootTest(classes = TestConfiguration.class) @EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class) @ActiveProfiles("test") -class ServletContextEnabledSmokeTest { +class ServletContextEnabledSmokeTest extends DataDirectoryTempSupport { private @Autowired ApplicationContext context; diff --git a/src/starters/webmvc/src/test/java/org/geoserver/cloud/config/main/GeoServerMainConfigurationSmokeTest.java b/src/starters/webmvc/src/test/java/org/geoserver/cloud/config/main/GeoServerMainConfigurationSmokeTest.java index 03e68cf35..3d2391d6a 100644 --- a/src/starters/webmvc/src/test/java/org/geoserver/cloud/config/main/GeoServerMainConfigurationSmokeTest.java +++ b/src/starters/webmvc/src/test/java/org/geoserver/cloud/config/main/GeoServerMainConfigurationSmokeTest.java @@ -9,6 +9,7 @@ import org.geoserver.catalog.Catalog; import org.geoserver.catalog.impl.CatalogImpl; +import org.geoserver.cloud.autoconfigure.servlet.DataDirectoryTempSupport; import org.geoserver.cloud.test.TestConfiguration; import org.geoserver.security.SecureCatalogImpl; import org.junit.jupiter.api.Test; @@ -18,11 +19,9 @@ import org.springframework.test.context.ActiveProfiles; /** Smoke test to load the main context without auto-configuration enabled and without security */ -@SpringBootTest( - classes = {TestConfiguration.class}, - properties = "reactive.feign.loadbalancer.enabled=false") +@SpringBootTest(classes = {TestConfiguration.class}) @ActiveProfiles("test") -class GeoServerMainConfigurationSmokeTest { +class GeoServerMainConfigurationSmokeTest extends DataDirectoryTempSupport { private @Autowired @Qualifier("rawCatalog") Catalog rawCatalog; private @Autowired @Qualifier("secureCatalog") Catalog secureCatalog; diff --git a/src/starters/webmvc/src/test/resources/application-test.yml b/src/starters/webmvc/src/test/resources/application-test.yml new file mode 100644 index 000000000..3fcb0cec2 --- /dev/null +++ b/src/starters/webmvc/src/test/resources/application-test.yml @@ -0,0 +1,18 @@ +spring: + main: + banner-mode: off + allow-bean-definition-overriding: true + allow-circular-references: true # false by default since spring-boot 2.6.0, breaks geoserver initialization + cloud.config.enabled: false + cloud.config.discovery.enabled: false +eureka.client.enabled: false + +geoserver: + backend: + data-directory: + enabled: true + location: # configured by TestConfiguration.java + +logging: + level: + ROOT: WARN diff --git a/src/starters/webmvc/src/test/resources/application.yml b/src/starters/webmvc/src/test/resources/application.yml deleted file mode 100644 index 8ea24a647..000000000 --- a/src/starters/webmvc/src/test/resources/application.yml +++ /dev/null @@ -1,34 +0,0 @@ -spring: - main: - banner-mode: off - allow-bean-definition-overriding: true - allow-circular-references: true # false by default since spring-boot 2.6.0, breaks geoserver initialization - cloud.bus.enabled: false - cloud.config.enabled: false - cloud.config.discovery.enabled: false -eureka.client.enabled: false - -geoserver: - security.enabled: true - backend: - data-directory: - enabled: true - location: ${data_directory:${java.io.tmpdir}/geoserver_cloud_data_directory} - jdbcconfig: - enabled: false - web.enabled: false - initdb: true - cache-directory: ${java.io.tmpdir}/geoserver-jdbcconfig-cache} - datasource: - driverClassname: org.h2.Driver - url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1 - username: sa - password: - -logging: - level: - ROOT: WARN - org.geoserver.platform: ERROR - org.geoserver.cloud: INFO - org.geoserver.cloud.config.factory: INFO - org.springframework.test: ERROR