[JAVA] Anforderungs- und Antwortprotokolle mit Spring Boot ausgeben

Ich habe einen Filter geschrieben, der Anforderungs- und Antwortprotokolle in Spring Boot ausgibt, damit ich ihn freigeben kann.

[Original] 1

Unterschied zum Original

Bezieht sich auf "AbstractRequestLoggingFilter" und "CommonsRequestLoggingFilter"

Ich möchte, dass Zabbix Protokolldateien aufnimmt und filtert

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.NullNode;

public class RequestResponseLoggingFilter extends OncePerRequestFilter {
    private static final List<MediaType> VISIBLE_TYPES = Arrays.asList(
                    MediaType.valueOf("text/*"),
                    MediaType.APPLICATION_FORM_URLENCODED,
                    MediaType.APPLICATION_JSON,
                    MediaType.APPLICATION_XML,
                    MediaType.valueOf("application/*+json"),
                    MediaType.valueOf("application/*+xml"),
                    MediaType.MULTIPART_FORM_DATA
                );
    private static final List<MediaType> FORM_TYPES = Arrays.asList(
                    MediaType.APPLICATION_FORM_URLENCODED,
                    MediaType.MULTIPART_FORM_DATA
                    );
    private final ThreadLocal<Map<String, Object>> localParams = ThreadLocal.withInitial(() -> new LinkedHashMap<>());

    @Autowired
    private ObjectMapper mapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
        if (isAsyncDispatch(request)) {
            filterChain.doFilter(request, response);
        } else {
            doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
        }
    }

    protected void doFilterWrapped(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response, FilterChain filterChain) throws ServletException, IOException {
        try {
            beforeRequest(request, response);
            filterChain.doFilter(request, response);
        }
        finally {
            afterRequest(request, response);
            outputParams();
            localParams.remove();
            response.copyBodyToResponse();
        }
    }

    protected void beforeRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
        if (logger.isDebugEnabled()) {
            throwable("logRequestHeader", () -> logRequestHeader(request));
        }
    }

    protected void afterRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
        if (logger.isDebugEnabled()) {
            throwable("logRequestBody", () -> logRequestBody(request));
            throwable("logResponse", () -> logResponse(response));
        }
    }

    private void logRequestHeader(ContentCachingRequestWrapper request) {
        localParams.get().put("client", request.getRemoteAddr());
        String queryString = request.getQueryString();
        if (queryString == null) {
            localParams.get().put("request", request.getMethod() + " " + request.getRequestURI());
        }
        else {
            localParams.get().put("request", request.getMethod() + " " + request.getRequestURI() + "?" + queryString);
        }
        Map<String, List<String>> headers = new LinkedHashMap<>();
        Collections.list(request.getHeaderNames()).forEach(headerName ->
            Collections.list(request.getHeaders(headerName)).forEach(headerValue -> {
                if (headers.containsKey(headerName)) {
                    headers.get(headerName).add(headerValue);
                }
                else {
                    headers.put(headerName, new ArrayList<>());
                    headers.get(headerName).add(headerValue);
                }
            }));
        localParams.get().put("requestHeaders", headers);
        SecurityContext ctx = SecurityContextHolder.getContext();
        if (null != ctx) {
            localParams.get().put("authentication", convertTo(ctx.getAuthentication()));
        }
     }

    private void logRequestBody(ContentCachingRequestWrapper request) throws ServletException, IOException {
        Collection<Part> parts = getRequestParts(request);
        if (parts.isEmpty()) {
            byte[] content = request.getContentAsByteArray();
            if (content.length > 0) {
                logContent(content, request.getContentType(), request.getCharacterEncoding(), "requestBody");
            }
            else {
                localParams.get().put("requestBody", "[empty body]");
            }
        }
        else {
            Map<String, Object> contents = new LinkedHashMap<>();
            parts.stream().forEach(p -> {
                String name = p.getName();
                try {
                    byte[] content = FileCopyUtils.copyToByteArray(p.getInputStream());
                    contents.put(name, logContent(content, p.getContentType(), "UTF-8"));
                }
                catch (IOException e) {
                    String fileName = p.getSubmittedFileName();
                    if (StringUtils.hasText(fileName)) {
                        contents.put(name, String.format("fileName[%s]", fileName));
                    }
                    else {
                        contents.put(name, String.format("IOException[%s]", e.getMessage()));
                    }
                }
            });
            localParams.get().put("requestBody", contents);
        }
    }

    private Collection<Part> getRequestParts(ContentCachingRequestWrapper request) throws ServletException, IOException {
        MediaType mediaType = getMediaType(request.getContentType());
        boolean isPost = FORM_TYPES.stream().anyMatch(formType -> formType.includes(mediaType));
        return isPost ? request.getParts() : Collections.emptyList();
    }

    private void logResponse(ContentCachingResponseWrapper response) {
        int status = response.getStatus();
        localParams.get().put("response", status + " " + HttpStatus.valueOf(status).getReasonPhrase());
        Map<String, List<String>> headers = new LinkedHashMap<>();
        response.getHeaderNames().forEach(headerName ->
            response.getHeaders(headerName).forEach(headerValue -> {
                if (headers.containsKey(headerName)) {
                    headers.get(headerName).add(headerValue);
                }
                else {
                    headers.put(headerName, new ArrayList<>());
                    headers.get(headerName).add(headerValue);
                }
            }));
        localParams.get().put("responseHeaders", headers);
        byte[] content = response.getContentAsByteArray();
        if (content.length > 0) {
            logContent(content, response.getContentType(), response.getCharacterEncoding(), "responseBody");
        }
    }

    private void logContent(byte[] content, String contentType, String contentEncoding, String key) {
        localParams.get().put(key, logContent(content, contentType, contentEncoding));
    }

    private Object logContent(byte[] content, String contentType, String contentEncoding) {
        MediaType mediaType = getMediaType(contentType);
        boolean visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
        boolean json = mediaType.getSubtype().contains("json");
        if (visible) {
            try {
                String contentString = new String(content, contentEncoding);
                if (json) {
                    return mapper.readTree(contentString);
                }
                else {
                    return contentString;
                }
            }
            catch (IOException e) {
                return String.format("[%d bytes content]", content.length);
            }
        }
        else {
            return String.format("[%d bytes content]", content.length);
        }
    }

    private MediaType getMediaType(String contentType) {
        try {
            return MediaType.valueOf(contentType);
        }
        catch (IllegalArgumentException e) {
            return null;
        }
    }

    private void outputParams() {
        logger.debug(unparse(mapper, localParams.get()));
    }

    private JsonNode convertTo(Object obj) {
        NullNode node = mapper.getDeserializationConfig().getNodeFactory().nullNode();
        if (null == obj) {
            return node;
        }
        try {
            return mapper.readTree(mapper.writeValueAsString(obj));
        }
        catch (IOException e) {
            return node;
        }
    }

    private void throwable(String key, ThrowableExecutor method) {
        try {
            method.apply();
        }
        catch (Exception e) {
            localParams.get().put(key, String.format("%s[%s]", e.getClass().getSimpleName(), e.getMessage()));
        }
    }

    public static String unparse(ObjectMapper mapper, Object obj) {
        if (null == obj) {
            return "null";
        }
        if (null == mapper) {
            return String.valueOf(obj);
        }
        try {
            return mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            return e.getMessage();
        }
    }

    private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
        if (request instanceof ContentCachingRequestWrapper) {
            return (ContentCachingRequestWrapper) request;
        } else {
            return new ContentCachingRequestWrapper(request);
        }
    }

    private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse response) {
        if (response instanceof ContentCachingResponseWrapper) {
            return (ContentCachingResponseWrapper) response;
        } else {
            return new ContentCachingResponseWrapper(response);
        }
    }

    public static interface ThrowableExecutor {
        void apply() throws Exception;
    }
}

logback-spring.xml

logback-spring.xml


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml" />

    <appender name="REQUEST_LOGGING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/requestLogging.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/requestLogging.log.%d{yyyyMMdd}.gz</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t]%X{request_thread} %logger{45}:%L - %msg %n</pattern>
        </encoder>
    </appender>

    <logger name="RequestResponseLoggingFilter" level="DEBUG">
        <appender-ref ref="REQUEST_LOGGING_FILE"/>
    </logger>
</configuration>

Spring-Boot: Einstellungen ausführen

pom.xml


<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <executable>true</executable>
        <mainClass>Application</mainClass>
        <jvmArguments>
            -Dlogging.config=file:${project.build.outputDirectory}/logback-spring.xml
            -Dlogging.path=${project.build.directory}/log
        </jvmArguments>
        <includeSystemScope>true</includeSystemScope>
    </configuration>
</plugin>

Da das Format JSON ist, kann es mit "jq" formatiert werden

% tail -n 20 requestLogging.log|grep '/v1/info'|tail -n 1 |cut -d" " -f 7- |jq .

{
  "client": "0:0:0:0:0:0:0:1",
  "request": "GET /v1/info",
  "requestHeaders": {
    "host": [
      "localhost:8080"
    ],
    "user-agent": [
      "curl/7.54.0"
    ],
    "accept": [
      "*/*"
    ]
  },
  "authentication": {
    "authorities": [
      {
        "authority": "ROLE_ANONYMOUS"
      }
    ],
    "details": {
      "remoteAddress": "0:0:0:0:0:0:0:1",
      "sessionId": null
    },
    "authenticated": true,
    "principal": "anonymousUser",
    "keyHash": -184817062,
    "credentials": "",
    "name": "anonymousUser"
  },
  "requestBody": "[empty body]",
  "response": "200 OK",
  "responseHeaders": {
    "X-Content-Type-Options": [
      "nosniff"
    ],
    "X-XSS-Protection": [
      "1; mode=block"
    ],
    "Cache-Control": [
      "no-cache, no-store, max-age=0, must-revalidate"
    ],
    "Pragma": [
      "no-cache"
    ],
    "Expires": [
      "0"
    ],
    "X-Frame-Options": [
      "DENY"
    ]
  },
  "responseBody": "git info\n commit\n   branch: setup-logger\n   id: <commit>\n   time: 2018-04-05T21:27:51+09:00\n build\n   host: Macmini.local\n   time: 2018-04-05T23:29:45+09:00\n"
}

[Leistung !?] 1

Durch Anwenden dieses Filters wird der Antworttext vorübergehend im Speicher gehalten, wodurch mehr Speicher belegt wird. Wenn Sie eine große Antwort zurückgeben, leidet auch die Leistung. Daher ist es besser, es auf Debugging-Zwecke zu beschränken.

Der aktuelle Status der Backend-Rest-API lautet "log >>> (unüberwindbare Wand) >>> Speicher und Leistung"

Recommended Posts

Anforderungs- und Antwortprotokolle mit Spring Boot ausgeben
Spring Boot-Protokoll im JSON-Format ausgeben
Protokollausgabe der WebServiceTemplate-Anforderung / Antwort
Spring Boot Whitelabel-Fehlerseite und JSON-Antwort
Beispiel für die Ausgabe eines Parameterprotokollprotokolls Java & Spring MVC
Legen Sie den Kontextparameter in Spring Boot fest
Spring Boot 2 Multiprojekt mit Gradle
Melden Sie die Anforderung / Antwort von RestTemplate ab
Wichtige Änderungen in Spring Boot 1.5
NoHttpResponseException in Spring Boot + WireMock
Geben Sie das integrierte Tomcat-Zugriffsprotokoll mit Spring Boot an die Standardausgabe aus
Aufrufen und Verwenden der API in Java (Spring Boot)
Abrufen von Fehlerinformationen mithilfe von DefaultErrorAttributes und ErrorAttributeOptions in Spring Boot 2.3
Antwortheader werden in Spring Security 4.1 möglicherweise nicht korrekt ausgegeben
Frühlingsstiefel Hallo Welt in Eclipse
Spring Boot-Anwendungsentwicklung in Eclipse
Schreiben Sie den Testcode mit Spring Boot
Protokollausgabe in Datei in Java
Implementieren Sie die REST-API mit Spring Boot
Spring-Profilfunktion und Spring Boot application.properties
Was ist @Autowired im Spring Boot?
Implementieren Sie die Spring Boot-Anwendung in Gradle
Spring Data JPA SQL-Protokollausgabe
Verwendung von Thymeleaf mit Spring Boot
Einfache Entwicklung von Webanwendungen mit STS und Spring Boot. In 10 Minuten.
Ändern Sie die Spring Boot REST API-Anforderung / Antwort von CamelCase in SankeCase
Hochladen und Herunterladen von Dateien mit Ajax mit Spring Boot (ohne JQuery)
Vergleiche Hallo Welt! Mit Spring Boot mit Java, Kotlin und Groovy
Aktivieren Sie die Protokollausgabe sowohl für Dateien als auch für die Konsole mit log4j in Eclipse.
Erstellen Sie mit Intellij ein Spring Boot-Projekt und beenden Sie es sofort nach dem Start
Ausgabesystemprotokoll mit Spring AOP-Technologie
Starten Sie mit IntelliJ ein (altes) Spring Boot-Projekt
Konvertieren Sie Anforderungsparameter im Frühjahr in Enum
Erstellen Sie mit Gradle ein Spring Boot + Docker-Image
Statische Dateizugriffspriorität beim Spring Boot
Memorandum zum Herunterladen lokaler Dateien mit Spring Boot
HTTPS mit Spring Boot und Let's Encrypt
Erstellen Sie mit IntelliJ ein Java Spring Boot-Projekt
Lösen Sie die Thymeleaf-Syntaxprüfung in Spring Boot
[Trainieren! ] Zeigen Sie Hello World mit Spring Boot an
Verwenden Sie die DynamoDB-Abfragemethode mit Spring Boot
Antwortdaten direkt im Frühjahr schreiben
DI SessionScope Bean im Spring Boot 2-Filter
Fügen Sie Frühlingsstiefel und Gradle zur Sonnenfinsternis hinzu
Ändern Sie das Sitzungszeitlimit in Spring Boot
Ausgabe Bean als JSON im Frühjahr
Implementieren Sie eine benutzerdefinierte Validierung mit Anmerkungen in Spring Framework und geben Sie eine Nachricht aus
SameSite-Cookie im Spring Boot (Spring Web MVC + Tomcat)
Mehrteilige Anforderungsantwort von HttpURLConnection (und Apache Commons)
Geben Sie einfach Bilder mit Spring MVC ein und geben Sie sie aus
Testen Sie den Controller mit Mock MVC im Spring Boot
Asynchrone Verarbeitung mit regelmäßiger Ausführung in Spring Boot
Externe Veröffentlichung und Anmeldung von MySQL unter Ubuntu
Führen Sie ein Spring Boot-Projekt mit VS-Code aus
Bereiten Sie die Protokollausgabe mit log4j in Eclipse vor.
Verschiedene Korrespondenztabellen zwischen Spring Framework und Spring Boot
Servlet-Filter mit Spring Boot verwenden [Spring Boot 1.x, 2.x kompatibel]
So fügen Sie in Spring Boot einen Klassenpfad hinzu
Java-Tipps - Erstellen Sie mit Gradle ein Spring Boot-Projekt
So binden Sie mit einer Eigenschaftendatei in Spring Boot
Anmerkungen, die in Spring Boot-Aufgabenverwaltungstools verwendet werden