feat: initial commit

This commit is contained in:
Rick Rongen
2025-09-05 09:09:37 +02:00
commit 244bbb121e
18 changed files with 670 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/lib-1/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/lib-1/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/spring-boot-common-utils/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/spring-boot-parent/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/spring-boot-parent/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

14
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

7
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/spring-boot-parent" vcs="Git" />
</component>
</project>

29
lib-1/pom.xml Normal file
View File

@@ -0,0 +1,29 @@
<?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>lib-1</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.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

33
pom.xml Normal file
View File

@@ -0,0 +1,33 @@
<?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>
<scm>
<connection>scm:git:file://localhost/../sfg-sb-common-remote.git</connection>
<url>file://../sfg-sb-common-remote.git</url>
</scm>
<parent>
<groupId>com.sligrofoodgroup.spring-boot-common</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>spring-boot-parent/pom.xml</relativePath>
</parent>
<artifactId>common</artifactId>
<packaging>pom</packaging>
<modules>
<module>spring-boot-parent</module>
<module>lib-1</module>
<module>spring-boot-common-utils</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View 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>

View File

@@ -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);
}
}
}

View File

@@ -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 {
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}
}

View File

@@ -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())))));
}
}

View File

@@ -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.
}
}

View File

@@ -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

View File

@@ -0,0 +1,48 @@
<?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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.5</version>
<relativePath/>
</parent>
<groupId>com.sligrofoodgroup.spring-boot-common</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<sfg-sb-commons-version>${project.version}</sfg-sb-commons-version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<!--Internal dependencies-->
<dependency>
<groupId>com.sligrofoodgroup</groupId>
<artifactId>lib-1</artifactId>
<version>${sfg-sb-commons-version}</version>
</dependency>
<dependency>
<groupId>com.sligrofoodgroup.spring-boot-common</groupId>
<artifactId>spring-boot-common-utils</artifactId>
<version>${sfg-sb-commons-version}</version>
</dependency>
<!--Common dependencies-->
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>