feat: initial commit
This commit is contained in:
88
spring-boot-common-utils/pom.xml
Normal file
88
spring-boot-common-utils/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.sligrofoodgroup.spring-boot-common</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-boot-common-utils</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
TODO: is it safe to mark these as optional?
|
||||
Given these are used by @Component components (with a @Contitional)
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.annotation</groupId>
|
||||
<artifactId>jakarta.annotation-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>context-propagation</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webflux</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.sligrofoodgroup.sb.util.logging;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.MDC;
|
||||
import org.slf4j.event.Level;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
class AccessLogFilter {
|
||||
public static final String MDC_PREFIX = "req.";
|
||||
public static final String MDC_METHOD = MDC_PREFIX + "method";
|
||||
public static final String MDC_URI = MDC_PREFIX + "uri";
|
||||
public static final String MDC_REQUEST_ID = MDC_PREFIX + "requestId";
|
||||
public static final String MDC_REAL_IP = MDC_PREFIX + "realIp";
|
||||
public static final String MDC_STATUS_CODE = MDC_PREFIX + "statusCode";
|
||||
public static final String MDC_DURATION_MS = MDC_PREFIX + "durationMs";
|
||||
public static final String MDC_BASE_SITE_ID = MDC_PREFIX + "baseSiteId";
|
||||
|
||||
public static final String HTTP_HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
|
||||
public static final String HTTP_HEADER_X_REAL_IP = "X-Real-IP";
|
||||
public static final String HTTP_HEADER_X_REQUEST_ID = "X-Request-Id";
|
||||
public static final String HTTP_HEADER_BASE_SITE_ID = "X-Base-Site-Id";
|
||||
|
||||
@NotNull
|
||||
Map<String, Object> getPropagationContext() {
|
||||
return Map.of(MdcPropagator.MDC_TLA_NAME, MdcPropagator.getMdcContext());
|
||||
}
|
||||
|
||||
private static InetAddress getRemoteAddress(@NotNull HttpRequest httpRequest) {
|
||||
return getForwardedForAddress(httpRequest)
|
||||
.or(() -> getRealIpAddress(httpRequest))
|
||||
.or(() -> getFromRemoteAddress(httpRequest))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static @NotNull Optional<InetAddress> getFromRemoteAddress(@NotNull HttpRequest httpRequest) {
|
||||
try {
|
||||
if (httpRequest instanceof final ServletServerHttpRequest servletServerHttpRequest) {
|
||||
return Optional.ofNullable(servletServerHttpRequest.getRemoteAddress().getAddress());
|
||||
}
|
||||
} catch (final NoClassDefFoundError ncdfe) {
|
||||
// This will happen when we do not have WebMVC classes available
|
||||
}
|
||||
try {
|
||||
if (httpRequest instanceof final ServerHttpRequest serverHttpRequest) {
|
||||
return Optional.ofNullable(serverHttpRequest.getRemoteAddress())
|
||||
.map(InetSocketAddress::getAddress);
|
||||
}
|
||||
} catch (final NoClassDefFoundError ncdfe) {
|
||||
// This may happen if we do not have WebFlux classes available
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static @NotNull Optional<InetAddress> getRealIpAddress(@NotNull HttpRequest httpRequest) {
|
||||
return httpRequest.getHeaders()
|
||||
.getOrEmpty(HTTP_HEADER_X_REAL_IP)
|
||||
.stream()
|
||||
.findFirst()
|
||||
.map(host -> {
|
||||
try {
|
||||
return InetAddress.getByName(host);
|
||||
} catch (UnknownHostException e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static @NotNull Optional<InetAddress> getForwardedForAddress(@NotNull HttpRequest httpRequest) {
|
||||
return httpRequest.getHeaders()
|
||||
.getOrEmpty(HTTP_HEADER_X_FORWARDED_FOR)
|
||||
.stream()
|
||||
.flatMap(line -> Arrays.stream(line.split(",")).map(String::trim))
|
||||
.findFirst() // todo filter untrusted
|
||||
.map(host -> {
|
||||
try {
|
||||
return InetAddress.getByName(host);
|
||||
} catch (UnknownHostException e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getBaseSiteId(@NotNull HttpRequest httpRequest) {
|
||||
return Optional.ofNullable(httpRequest.getHeaders().getFirst(HTTP_HEADER_BASE_SITE_ID)).orElse("unknown");
|
||||
}
|
||||
|
||||
void fillMdc(@NotNull final HttpRequest request) {
|
||||
// todo, doesn't opentracing do this also? doesn't this conflict
|
||||
String requestId = Optional.ofNullable(request.getHeaders().getFirst(HTTP_HEADER_X_REQUEST_ID))
|
||||
.orElseGet(() -> UUID.randomUUID().toString());
|
||||
MDC.put(MDC_METHOD, request.getMethod().name());
|
||||
MDC.put(MDC_URI, request.getURI().toString());
|
||||
MDC.put(MDC_REQUEST_ID, requestId);
|
||||
MDC.put(MDC_REAL_IP, Optional.ofNullable(getRemoteAddress(request)).map(InetAddress::getHostAddress).orElse("unknown"));
|
||||
MDC.put(MDC_BASE_SITE_ID, getBaseSiteId(request));
|
||||
}
|
||||
|
||||
public AccessLogContex requestStart(@NotNull final HttpRequest request) {
|
||||
final AccessLogContex accessLogContex = new AccessLogContex(request);
|
||||
fillMdc(request);
|
||||
return accessLogContex;
|
||||
}
|
||||
|
||||
public void clearMdc() {
|
||||
MDC.remove(MDC_METHOD);
|
||||
MDC.remove(MDC_URI);
|
||||
MDC.remove(MDC_REQUEST_ID);
|
||||
MDC.remove(MDC_REAL_IP);
|
||||
}
|
||||
|
||||
public static class AccessLogContex {
|
||||
@NotNull
|
||||
private final HttpRequest request;
|
||||
@NotNull
|
||||
public final StopWatch stopWatch;
|
||||
|
||||
private AccessLogContex(@NotNull final HttpRequest request) {
|
||||
this.request = request;
|
||||
stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
}
|
||||
|
||||
void requestEnd(@Nullable final HttpStatusCode statusCode, @NotNull final Throwable throwable) {
|
||||
stopWatch.stop();
|
||||
log.makeLoggingEventBuilder(Level.WARN)
|
||||
.addKeyValue(MDC_STATUS_CODE, Optional.ofNullable(statusCode).map(HttpStatusCode::value).map(Object::toString).orElse("n/a"))
|
||||
.addKeyValue(MDC_DURATION_MS, stopWatch.getTotalTimeMillis())
|
||||
.setCause(throwable)
|
||||
.log("Request {} {} {}", request.getMethod(), request.getURI(), statusCode);
|
||||
}
|
||||
|
||||
void requestEnd(@Nullable final HttpStatusCode statusCode) {
|
||||
stopWatch.stop();
|
||||
log.makeLoggingEventBuilder(Level.WARN)
|
||||
.addKeyValue(MDC_STATUS_CODE, Optional.ofNullable(statusCode).map(HttpStatusCode::value).map(Object::toString).orElse("n/a"))
|
||||
.addKeyValue(MDC_DURATION_MS, stopWatch.getTotalTimeMillis())
|
||||
.log("Request {} {} {}", request.getMethod(), request.getURI(), statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sligrofoodgroup.sb.util.logging;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Import({
|
||||
ServletAccessLogFilter.class,
|
||||
WebFluxAccessLogFilter.class,
|
||||
MdcLoggingConfiguration.class,
|
||||
MissingMdcLoggingConfiguration.class,
|
||||
WebClientRequestIdForwarder.class,
|
||||
})
|
||||
public @interface EnableSfgSbLogging {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sligrofoodgroup.sb.util.logging;
|
||||
|
||||
import io.micrometer.context.ContextRegistry;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnClass(ContextRegistry.class)
|
||||
class MdcLoggingConfiguration {
|
||||
@PostConstruct
|
||||
public void registerMdcPropagation() {
|
||||
log.info("Enabling MDC propagation");
|
||||
ContextRegistry.getInstance().registerThreadLocalAccessor(new MdcPropagator());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.sligrofoodgroup.sb.util.logging;
|
||||
|
||||
|
||||
import io.micrometer.context.ThreadLocalAccessor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
class MdcPropagator implements ThreadLocalAccessor<Map<String, String>> {
|
||||
public static final String MDC_TLA_NAME = MdcPropagator.class.getName();
|
||||
|
||||
@NotNull
|
||||
public static Map<String, String> getMdcContext() {
|
||||
return Optional.ofNullable(MDC.getCopyOfContextMap())
|
||||
.map(Map::entrySet)
|
||||
.orElse(Collections.emptySet())
|
||||
.stream()
|
||||
.filter(e -> e.getKey().startsWith(AccessLogFilter.MDC_PREFIX))
|
||||
.filter(e -> Objects.nonNull(e.getValue()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Object key() {
|
||||
return MDC_TLA_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Map<String, String> getValue() {
|
||||
return getMdcContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(@NotNull Map<String, String> value) {
|
||||
value.forEach(MDC::put);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue() {
|
||||
Optional.ofNullable(MDC.getCopyOfContextMap())
|
||||
.map(Map::keySet)
|
||||
.orElse(Collections.emptySet())
|
||||
.stream()
|
||||
.filter(key -> key.startsWith(AccessLogFilter.MDC_PREFIX))
|
||||
.forEach(MDC::remove);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.sligrofoodgroup.sb.util.logging;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnMissingClass("io.micrometer.context.ContextRegistry")
|
||||
class MissingMdcLoggingConfiguration {
|
||||
@PostConstruct
|
||||
public void registerMdcPropagation() {
|
||||
log.warn("Missing Micrometer Context Propagator dependency, MDC is not propagated");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.sligrofoodgroup.sb.util.logging;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
class ServletAccessLogFilter extends OncePerRequestFilter {
|
||||
|
||||
private final AccessLogFilter accessLogFilterLogger = new AccessLogFilter();
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
|
||||
AccessLogFilter.AccessLogContex accessLogContex = accessLogFilterLogger.requestStart(new ServletServerHttpRequest(request));
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
accessLogContex.requestEnd(HttpStatusCode.valueOf(response.getStatus()));
|
||||
} catch (Exception throwable) {
|
||||
accessLogContex.requestEnd(HttpStatusCode.valueOf(response.getStatus()), throwable);
|
||||
throw throwable;
|
||||
} finally {
|
||||
accessLogFilterLogger.clearMdc();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sligrofoodgroup.sb.util.logging;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnClass(WebClient.class)
|
||||
public class WebClientRequestIdForwarder {
|
||||
|
||||
@Bean
|
||||
public WebClientCustomizer webClientCustomizer() {
|
||||
return webClientBuilder ->
|
||||
webClientBuilder.filter(((request, next) ->
|
||||
Mono.deferContextual((contextView ->
|
||||
next.exchange(ClientRequest
|
||||
.from(request)
|
||||
.headers(httpHeaders -> {
|
||||
contextView.getOrEmpty(AccessLogFilter.MDC_REQUEST_ID)
|
||||
.ifPresent(requestId -> httpHeaders.set(AccessLogFilter.HTTP_HEADER_X_REQUEST_ID, (String) requestId));
|
||||
})
|
||||
.build())))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.sligrofoodgroup.sb.util.logging;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||
class WebFluxAccessLogFilter implements WebFilter {
|
||||
|
||||
private final AccessLogFilter accessLogger = new AccessLogFilter();
|
||||
|
||||
@Override
|
||||
public @NotNull Mono<Void> filter(@NotNull ServerWebExchange exchange, @NotNull WebFilterChain chain) {
|
||||
final AccessLogFilter.AccessLogContex accessLogContex = this.accessLogger.requestStart(exchange.getRequest());
|
||||
Mono<Void> filteredChain = chain.filter(exchange)
|
||||
.doOnError(throwable -> accessLogContex.requestEnd(exchange.getResponse().getStatusCode(), throwable))
|
||||
.doOnSuccess(v -> accessLogContex.requestEnd(exchange.getResponse().getStatusCode()))
|
||||
.contextWrite(Context.of(this.accessLogger.getPropagationContext()));
|
||||
this.accessLogger.clearMdc();
|
||||
return filteredChain;
|
||||
// NOTE: clearing the MDC isn't really possible in reactive context
|
||||
// doFinally is invoked at the end of the mono chain in the thread the mono gets resolved.
|
||||
// This makes doFinally unable to clear the MDC.
|
||||
// Furthermore, this thread could already be handling a new request at this point in time.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
logging:
|
||||
level:
|
||||
nl:
|
||||
sligro: INFO
|
||||
com:
|
||||
sligrofoodgroup: INFO
|
||||
spring:
|
||||
reactor:
|
||||
context-propagation: auto
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: '!local'
|
||||
logging:
|
||||
structured:
|
||||
format:
|
||||
console: logstash
|
||||
#---
|
||||
#spring:
|
||||
# config:
|
||||
# activate:
|
||||
# on-profile: local
|
||||
Reference in New Issue
Block a user