diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/CustomAppender.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/CustomAppender.java new file mode 100644 index 000000000..46d10885b --- /dev/null +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/CustomAppender.java @@ -0,0 +1,61 @@ +package fr.dossierfacile.api.front.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.Context; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class CustomAppender extends AppenderBase { + private static final Map> logsByRequestId = new ConcurrentHashMap<>(); + + @Override + protected void append(ILoggingEvent eventObject) { + String requestId = MDC.get("request_id"); + if (requestId != null) { + String message = eventObject.getFormattedMessage(); + + if (eventObject.getLevel().isGreaterOrEqual(Level.ERROR)) { + IThrowableProxy throwableProxy = eventObject.getThrowableProxy(); + if (throwableProxy != null) { + StringBuilder stackTrace = new StringBuilder(); + for (StackTraceElementProxy line : throwableProxy.getStackTraceElementProxyArray()) { + stackTrace.append(line).append("\n"); + } + message += "\n" + stackTrace; + } + } + + LogModel log = new LogModel(message, eventObject.getLevel()); + logsByRequestId.computeIfAbsent(requestId, id -> new ArrayList<>()) + .add(log); + } + } + + public static List getLogsForRequest(String requestId) { + return logsByRequestId.getOrDefault(requestId, List.of()); + } + + public static void clearLogsForRequest(String requestId) { + logsByRequestId.remove(requestId); + } + + public static void attachToRootLogger() { + Logger rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + CustomAppender customAppender = new CustomAppender(); + customAppender.setName("CUSTOM"); + Context context = rootLogger.getLoggerContext(); + customAppender.setContext(context); + customAppender.start(); + rootLogger.addAppender(customAppender); + } +} diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/GlobalExceptionHandler.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/GlobalExceptionHandler.java new file mode 100644 index 000000000..6dd39131d --- /dev/null +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/GlobalExceptionHandler.java @@ -0,0 +1,18 @@ +package fr.dossierfacile.api.front.log; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.error("Unhandled exception: ", e); + return new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogAggregationFilter.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogAggregationFilter.java new file mode 100644 index 000000000..d372efb0b --- /dev/null +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogAggregationFilter.java @@ -0,0 +1,72 @@ +package fr.dossierfacile.api.front.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggingEvent; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import net.logstash.logback.appender.LogstashTcpSocketAppender; +import org.jetbrains.annotations.NotNull; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class LogAggregationFilter extends OncePerRequestFilter { + private LogstashTcpSocketAppender logstashAppender; + + @PostConstruct + public void init() { + Logger rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + logstashAppender = (LogstashTcpSocketAppender) rootLogger.getAppender("LOGSTASH"); + if (logstashAppender == null) { + throw new IllegalStateException("Logstash appender (LOGSTASH) not found in Logback configuration."); + } + } + + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String requestId = MDC.get("request_id"); + try { + filterChain.doFilter(request, response); + } finally { + List logs = CustomAppender.getLogsForRequest(requestId); + String logMessage = logs.stream().map(LogModel::getMessage).collect(Collectors.joining()); + + String enrichedLogs = String.format( + "Request completed: URI:%s, Method:%s, Status:%d, Logs:%s", + request.getRequestURI(), + request.getMethod(), + response.getStatus(), + logMessage + ); + + Level logLevel = logs.stream().map(LogModel::getLevel).max(Comparator.comparingInt(Level::toInt)).orElse(Level.INFO); + + LoggingEvent enrichedEvent = new LoggingEvent(); + enrichedEvent.setTimeStamp(System.currentTimeMillis()); + enrichedEvent.setLoggerName("AggregatedHttpLogger"); + enrichedEvent.setLevel(logLevel); + enrichedEvent.setThreadName(Thread.currentThread().getName()); + enrichedEvent.setMessage(enrichedLogs); + + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + enrichedEvent.setLoggerContext(loggerContext); + + logstashAppender.doAppend(enrichedEvent); + + CustomAppender.clearLogsForRequest(requestId); + } + } +} diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogConfiguration.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogConfiguration.java new file mode 100644 index 000000000..893bee565 --- /dev/null +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogConfiguration.java @@ -0,0 +1,14 @@ +package fr.dossierfacile.api.front.log; + +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +@Component +public class LogConfiguration { + + @PostConstruct + public void setupCustomLogging() { + CustomAppender.attachToRootLogger(); + } +} \ No newline at end of file diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogModel.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogModel.java new file mode 100644 index 000000000..4b5f255ea --- /dev/null +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/log/LogModel.java @@ -0,0 +1,15 @@ +package fr.dossierfacile.api.front.log; + +import ch.qos.logback.classic.Level; +import lombok.Getter; + +@Getter +public class LogModel { + String message; + Level level; + + public LogModel(String message, Level level) { + this.message = message; + this.level = level; + } +} diff --git a/dossierfacile-api-tenant/src/main/resources/logback-spring-delayed.xml b/dossierfacile-api-tenant/src/main/resources/logback-spring-delayed.xml index 135363ba8..d353f25e5 100644 --- a/dossierfacile-api-tenant/src/main/resources/logback-spring-delayed.xml +++ b/dossierfacile-api-tenant/src/main/resources/logback-spring-delayed.xml @@ -56,9 +56,11 @@ + +