Spring Boot - Comment enregistrer toutes les demandes et réponses avec des exceptions en un seul endroit?

219

Je travaille sur l'api de repos avec Spring Boot. J'ai besoin de consigner toutes les demandes avec des paramètres d'entrée (avec des méthodes, par exemple GET, POST, etc.), le chemin de la demande, la chaîne de requête, la méthode de classe correspondante à cette demande, ainsi que la réponse de cette action, à la fois le succès et les erreurs.

À titre d'exemple:

demande réussie:

http://example.com/api/users/1

Le journal devrait ressembler à ceci:

{
   HttpStatus: 200,
   path: "api/users/1",
   method: "GET",
   clientIp: "0.0.0.0",
   accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
   method: "UsersController.getUser",
   arguments: {
     id: 1 
   },
   response: {
      user: {
        id: 1,
        username: "user123",
        email: "[email protected]"   
      }
   },
   exceptions: []       
}

Ou demande avec erreur:

http://example.com/api/users/9999

Le journal devrait ressembler à ceci:

    {
       HttpStatus: 404,
       errorCode: 101,                 
       path: "api/users/9999",
       method: "GET",
       clientIp: "0.0.0.0",
       accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
       method: "UsersController.getUser",
       arguments: {
         id: 9999 
       },
       returns: {            
       },
       exceptions: [
         {
           exception: "UserNotFoundException",
           message: "User with id 9999 not found",
           exceptionId: "adhaskldjaso98d7324kjh989",
           stacktrace: ...................    
       ]       
    }

Je souhaite que la demande / réponse soit une entité unique, avec des informations personnalisées liées à cette entité, à la fois dans les cas de réussite et d'erreur.

Quelle est la meilleure pratique au printemps pour y parvenir, peut-être avec des filtres? si oui, pouvez-vous donner un exemple concret?

(J'ai joué avec @ControllerAdvice et @ExceptionHandler, mais comme je l'ai mentionné, je dois gérer toutes les demandes de réussite et d'erreur en un seul endroit (et un seul journal)).

Teimuraz
la source
Probablement via un ServletFilter de journalisation (par exemple stackoverflow.com/a/2171633/995891 ), HandlerInterceptormais cela peut ne pas fonctionner correctement avec la journalisation de la réponse comme mentionné dans la réponse: concretepage.com/spring/spring-mvc/… - HandlerInterceptor a accès à la méthode (méthode: "UsersController.getUser") cependant. Ce n'est pas connu dans un filtre de servlet.
zapl
1
malgré tout, même si vous ajoutez un filtre ou une autre solution au niveau de la couche application, vous n'enregistrerez pas toute la demande, par exemple l'erreur du serveur HTTP 500 ne sera pas enregistrée, car au moment où une exception non gérée sera levée au niveau de la couche application, la page d'erreur par défaut du tomcat intégré sera affichée après avoir avalé l'exception et bien sûr ne conservera pas le journal. De plus, si vous cochez la réponse de user1817243, en cas d'exception, il n'enregistrera pas de nouveau la demande mais il enregistrera l'exception (!!).
AntJavaDev
Ce format de journal doit-il être cohérent avec chaque caractère que vous avez écrit? On dirait qu'une traduction JSON serait optimale dans votre cas: LogClass{ getRequestAndSaveIt()} Gson.toJson(LogClass)comme pseudocode
Vale
1
Les futurs lecteurs pourront bénéficier de ma réponse (url à suivre dans ce commentaire). Fondamentalement, j'ai pu franken-stein ensemble différents messages sur cette question. VEUILLEZ considérer la réponse de l'actionneur (dans les réponses ci-dessous) avant de l'essayer à la main. Mais la réponse que je poste permet à "400, 404, 500" (tout / tout) d'être enregistré, mais en définissant la priorité de commande sur la priorité la plus basse (ou à l'intérieur de "8" si vous regardez le code). stackoverflow.com/questions/10210645/…
granadaCoder
J'ai suivi les documents du printemps sur la journalisation à partir d'ici: docs.spring.io/spring-boot/docs/current/reference/html/…
T04435

Réponses:

150

N'écrivez pas d'intercepteurs, de filtres, de composants, d'aspects, etc., c'est un problème très courant qui a été résolu plusieurs fois.

Spring Boot dispose d'un module appelé Actuator , qui permet de déconnecter les requêtes HTTP de la boîte. Il y a un point de terminaison mappé à /trace(SB1.x) ou/actuator/httptrace (SB2.0 +) qui vous montrera les 100 dernières requêtes HTTP. Vous pouvez le personnaliser pour enregistrer chaque demande ou écrire dans une base de données.

Pour obtenir les points de terminaison que vous souhaitez, vous aurez besoin de la dépendance Spring-boot-starter-actuator, ainsi que de la "liste blanche" des points de terminaison que vous recherchez, et éventuellement configurer ou désactiver la sécurité pour cela.

De plus, où cette application s'exécutera-t-elle? Allez-vous utiliser un PaaS? Les fournisseurs d'hébergement, Heroku par exemple, fournissent la journalisation des demandes dans le cadre de leur service et vous n'avez alors pas besoin de faire de codage.

SergeyB
la source
4
plus de détails? J'ai trouvé github.com/spring-projects/spring-boot/tree/master/… , mais pas grand-chose au-delà.
Tom Howard
16
Cela ne peut pas être utilisé pour le débogage: les demandes non authentifiées (par exemple avec la sécurité Spring) ne sont pas enregistrées.
bekce
11
En fait, Actuator n'a pas de composants spécifiques pour activer la journalisation http. / trace - affiche uniquement les N dernières requêtes.
Vladimir Filipchenko
18
@ike_love, comment configurer l'actionneur de telle sorte qu'il enregistre la demande (également le corps POST) à déposer?
11
Trace n'enregistrera pas le corps de la demande et de la réponse pour vous .... tout le reste (en-tête, etc.) sauf ceux-ci.
Lekkie
95

Spring fournit déjà un filtre qui fait ce travail. Ajoutez le bean suivant à votre configuration

@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
    CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
    loggingFilter.setIncludeClientInfo(true);
    loggingFilter.setIncludeQueryString(true);
    loggingFilter.setIncludePayload(true);
    loggingFilter.setMaxPayloadLength(64000);
    return loggingFilter;
}

N'oubliez pas de changer le niveau de journalisation org.springframework.web.filter.CommonsRequestLoggingFilteren DEBUG.

Yogesh Badke
la source
75
Notez qu'il n'enregistre pas les réponses, seulement les demandes.
Wim Deblauwe
1
Il n'y a que des demandes. Comment enregistrer les corps des réponses à l'aide de CommonsRequestLoggingFilter?
user2602807
3
De plus, cela ne consigne pas l'exception
BhendiGawaar
Eh bien, cela est attendu car il s'agit d'un filtre de journalisation des demandes. Plus d'informations à ce sujet ici: docs.spring.io/spring/docs/current/javadoc-api/org/…
Yogesh Badke
4
Si vous avez un corps JSON volumineux, définissez la longueur de la charge utile sur un grand nombre pour consigner l'ensemble du corps de la demande. loggingFilter.setMaxPayloadLength (100000);
Venkatesh Nannan
58

Vous pouvez utiliser javax.servlet.Filters'il n'était pas nécessaire de consigner la méthode java qui a été exécutée.

Mais avec cette exigence que vous avez des informations d'accès stockées dans handlerMappingdes DispatcherServlet. Cela dit, vous pouvez remplacer DispatcherServletpour effectuer la journalisation de la paire demande / réponse.

Vous trouverez ci-dessous un exemple d'idée qui peut être encore améliorée et adaptée à vos besoins.

public class LoggableDispatcherServlet extends DispatcherServlet {

    private final Log logger = LogFactory.getLog(getClass());

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }
        HandlerExecutionChain handler = getHandler(request);

        try {
            super.doDispatch(request, response);
        } finally {
            log(request, response, handler);
            updateResponse(response);
        }
    }

    private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, HandlerExecutionChain handler) {
        LogMessage log = new LogMessage();
        log.setHttpStatus(responseToCache.getStatus());
        log.setHttpMethod(requestToCache.getMethod());
        log.setPath(requestToCache.getRequestURI());
        log.setClientIp(requestToCache.getRemoteAddr());
        log.setJavaMethod(handler.toString());
        log.setResponse(getResponsePayload(responseToCache));
        logger.info(log);
    }

    private String getResponsePayload(HttpServletResponse response) {
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {

            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                int length = Math.min(buf.length, 5120);
                try {
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                }
                catch (UnsupportedEncodingException ex) {
                    // NOOP
                }
            }
        }
        return "[unknown]";
    }

    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
            WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        responseWrapper.copyBodyToResponse();
    }

}

HandlerExecutionChain - contient les informations sur le gestionnaire de requêtes.

Vous pouvez ensuite enregistrer ce répartiteur comme suit:

    @Bean
    public ServletRegistrationBean dispatcherRegistration() {
        return new ServletRegistrationBean(dispatcherServlet());
    }

    @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet() {
        return new LoggableDispatcherServlet();
    }

Et voici l'exemple de journaux:

http http://localhost:8090/settings/test
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=500, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475814077,"status":500,"error":"Internal Server Error","exception":"java.lang.RuntimeException","message":"org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.RuntimeException","path":"/settings/test"}'}

http http://localhost:8090/settings/params
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=200, path='/settings/httpParams', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public x.y.z.DTO x.y.z.Controller.params()] and 3 interceptors', arguments=null, response='{}'}

http http://localhost:8090/123
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=404, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475840592,"status":404,"error":"Not Found","message":"Not Found","path":"/123"}'}

METTRE À JOUR

En cas d'erreurs, Spring gère automatiquement les erreurs. Par conséquent, BasicErrorController#errors'affiche comme gestionnaire de demandes. Si vous souhaitez conserver le gestionnaire de requêtes d'origine, vous pouvez remplacer ce comportement lors de l' appel spring-webmvc-4.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971avant #processDispatchResult, pour mettre en cache le gestionnaire d'origine.

hahn
la source
2
que se passe-t-il lorsque la réponse est un flux et que le flux ne prend pas en charge la recherche? Est-ce que ce qui précède fonctionnera toujours?
Tom Howard
Je me fiche de la méthode invoquée, juste des données reçues et envoyées. Le filtre semble m'orienter dans la bonne direction et la réponse de @ ike_love m'a dirigé vers github.com/spring-projects/spring-boot/blob/master/…
Tom Howard
@ TomHoward AFAIK, il n'y a pas de "journalisation des réponses" au printemps. Par conséquent, vous pouvez étendre WebRequestTraceFilter ou AbstractRequestLoggingFilter en ajoutant une logique de journalisation des réponses.
hahn
Fonctionne très bien!
Pavel Vlasov
@hahn pourquoi avez-vous utilisé le servlet Dispatcher pour cela? la même connexion ne peut-elle pas être ajoutée avec le filtre dans doFilter?
BhendiGawaar
39

La bibliothèque Logbook est spécialement conçue pour la journalisation des requêtes et réponses HTTP. Il prend en charge Spring Boot à l'aide d'une bibliothèque de démarrage spéciale.

Pour activer la journalisation dans Spring Boot, il vous suffit d'ajouter la bibliothèque aux dépendances de votre projet. Par exemple, en supposant que vous utilisez Maven:

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>logbook-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

Par défaut, la sortie de journalisation ressemble à ceci:

{
  "origin" : "local",
  "correlation" : "52e19498-890c-4f75-a06c-06ddcf20836e",
  "status" : 200,
  "headers" : {
    "X-Application-Context" : [
      "application:8088"
    ],
    "Content-Type" : [
      "application/json;charset=UTF-8"
    ],
    "Transfer-Encoding" : [
      "chunked"
    ],
    "Date" : [
      "Sun, 24 Dec 2017 13:10:45 GMT"
    ]
  },
  "body" : {
    "thekey" : "some_example"
  },
  "duration" : 105,
  "protocol" : "HTTP/1.1",
  "type" : "response"
}

Toutefois, il ne génère pas le nom de classe qui gère la demande. La bibliothèque possède quelques interfaces pour écrire des enregistreurs personnalisés.

sihaya
la source
4
ajouté en tant que dépendance à une application minimale de démarrage de printemps et a essayé de s'exécuter - aucun changement, aucune sortie de journalisation dans mon application. Je pense qu'il y a des dépendances ou des classes supplémentaires dont il a besoin? L'enregistrer en tant que filtre ne semble rien faire non plus.
eis
1
@eis Vous devez l'enregistrer en tant que filtre comme expliqué dans la documentation ici. github.com/zalando/logbook
Pratik Singhal
2
Le document Logbook dit: "Logbook est livré avec une configuration automatique pratique pour les utilisateurs de Spring Boot. Il configure automatiquement toutes les parties suivantes avec des valeurs par défaut raisonnables." Mais ça ne marche pas.
Leos Literak
5
@LeosLiterak Je pense que vous devez ajouter logging.level.org.zalando.logbook=TRACE à votre application.properties(comme indiqué dans le Readme)
TolkienWASP
2
La configuration automatique du journal de bord ne semble pas fonctionner pour Spring-Boot v2.0.5
Yashveer Rana
26

J'avais défini le niveau de connexion application.propertiespour imprimer les demandes / réponses, l'url de la méthode dans le fichier journal

logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=INFO
logging.file=D:/log/myapp.log

J'avais utilisé Spring Boot.

Ankit
la source
2
Oui, vous avez raison - il s'agit d'une réponse valide pour les requêtes get se connectant au même fichier journal avec tous les autres résultats. Cependant, @moreo a demandé de se connecter GET, POST, etc. et dans le fichier séparé (si je comprends bien)
Manushin Igor
4
J'aime celui la. zéro drame
Quirino Gervacio
1
Si vous souhaitez que les en-têtes soient inclus dans le journal, vous devez ajouter: "spring.http.log-request-details = true" à votre fichier application.properties.
jfajunior
20

Voici comment je le fais au printemps, les données reposent en utilisant org.springframework.web.util.ContentCachingRequestWrapper et org.springframework.web.util.ContentCachingResponseWrapper

/**
 * Doogies very cool HTTP request logging
 *
 * There is also {@link org.springframework.web.filter.CommonsRequestLoggingFilter}  but it cannot log request method
 * And it cannot easily be extended.
 *
 * https://mdeinum.wordpress.com/2015/07/01/spring-framework-hidden-gems/
 * http://stackoverflow.com/questions/8933054/how-to-read-and-copy-the-http-servlet-response-output-stream-content-for-logging
 */
public class DoogiesRequestLogger extends OncePerRequestFilter {

  private boolean includeResponsePayload = true;
  private int maxPayloadLength = 1000;

  private String getContentAsString(byte[] buf, int maxLength, String charsetName) {
    if (buf == null || buf.length == 0) return "";
    int length = Math.min(buf.length, this.maxPayloadLength);
    try {
      return new String(buf, 0, length, charsetName);
    } catch (UnsupportedEncodingException ex) {
      return "Unsupported Encoding";
    }
  }

  /**
   * Log each request and respponse with full Request URI, content payload and duration of the request in ms.
   * @param request the request
   * @param response the response
   * @param filterChain chain of filters
   * @throws ServletException
   * @throws IOException
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    long startTime = System.currentTimeMillis();
    StringBuffer reqInfo = new StringBuffer()
     .append("[")
     .append(startTime % 10000)  // request ID
     .append("] ")
     .append(request.getMethod())
     .append(" ")
     .append(request.getRequestURL());

    String queryString = request.getQueryString();
    if (queryString != null) {
      reqInfo.append("?").append(queryString);
    }

    if (request.getAuthType() != null) {
      reqInfo.append(", authType=")
        .append(request.getAuthType());
    }
    if (request.getUserPrincipal() != null) {
      reqInfo.append(", principalName=")
        .append(request.getUserPrincipal().getName());
    }

    this.logger.debug("=> " + reqInfo);

    // ========= Log request and response payload ("body") ========
    // We CANNOT simply read the request payload here, because then the InputStream would be consumed and cannot be read again by the actual processing/server.
    //    String reqBody = DoogiesUtil._stream2String(request.getInputStream());   // THIS WOULD NOT WORK!
    // So we need to apply some stronger magic here :-)
    ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
    ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);

    filterChain.doFilter(wrappedRequest, wrappedResponse);     // ======== This performs the actual request!
    long duration = System.currentTimeMillis() - startTime;

    // I can only log the request's body AFTER the request has been made and ContentCachingRequestWrapper did its work.
    String requestBody = this.getContentAsString(wrappedRequest.getContentAsByteArray(), this.maxPayloadLength, request.getCharacterEncoding());
    if (requestBody.length() > 0) {
      this.logger.debug("   Request body:\n" +requestBody);
    }

    this.logger.debug("<= " + reqInfo + ": returned status=" + response.getStatus() + " in "+duration + "ms");
    if (includeResponsePayload) {
      byte[] buf = wrappedResponse.getContentAsByteArray();
      this.logger.debug("   Response body:\n"+getContentAsString(buf, this.maxPayloadLength, response.getCharacterEncoding()));
    }

    wrappedResponse.copyBodyToResponse();  // IMPORTANT: copy content of response back into original response

  }


}
Robert
la source
18

Si cela ne vous dérange pas d'essayer Spring AOP, c'est quelque chose que j'ai exploré à des fins de journalisation et cela fonctionne assez bien pour moi. Il n'enregistrera pas les demandes qui n'ont pas été définies et les tentatives de demande ont échoué.

Ajoutez ces trois dépendances

spring-aop, aspectjrt, aspectjweaver

Ajoutez ceci à votre fichier de configuration xml <aop:aspectj-autoproxy/>

Créer une annotation pouvant être utilisée comme point de coupe

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface EnableLogging {
ActionType actionType();
}

Annotez maintenant toutes vos méthodes d'API de repos que vous souhaitez enregistrer

@EnableLogging(actionType = ActionType.SOME_EMPLOYEE_ACTION)
@Override
public Response getEmployees(RequestDto req, final String param) {
...
}

Passons maintenant à l'Aspect. scanner les composants du package dans lequel se trouve cette classe.

@Aspect
@Component
public class Aspects {

@AfterReturning(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", returning = "result")
public void auditInfo(JoinPoint joinPoint, Object result, EnableLogging enableLogging, Object reqArg, String reqArg1) {

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
            .getRequest();

    if (result instanceof Response) {
        Response responseObj = (Response) result;

    String requestUrl = request.getScheme() + "://" + request.getServerName()
                + ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
                + "?" + request.getQueryString();

String clientIp = request.getRemoteAddr();
String clientRequest = reqArg.toString();
int httpResponseStatus = responseObj.getStatus();
responseObj.getEntity();
// Can log whatever stuff from here in a single spot.
}


@AfterThrowing(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", throwing="exception")
public void auditExceptionInfo(JoinPoint joinPoint, Throwable exception, EnableLogging enableLogging, Object reqArg, String reqArg1) {

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
            .getRequest();

    String requestUrl = request.getScheme() + "://" + request.getServerName()
    + ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
    + "?" + request.getQueryString();

    exception.getMessage();
    exception.getCause();
    exception.printStackTrace();
    exception.getLocalizedMessage();
    // Can log whatever exceptions, requests, etc from here in a single spot.
    }
}

Le conseil @AfterReturning s'exécute lorsqu'une exécution de méthode correspondante revient normalement.

Le conseil @AfterThrowing s'exécute lorsqu'une exécution de méthode correspondante se termine en lançant une exception.

Si vous voulez lire en détail, lisez ceci. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

OIE grise
la source
1
Cela enregistre l'invocation de la méthode, pas ce qui a été réellement reçu et envoyé au niveau HTTP.
Tom Howard
1
Comment rédiger le CORPS de demande? Dans mon cas, c'est POST BODY. on request.getReader ou getInputStream J'obtiens une erreur indiquant que le flux est fermé.
13

Après avoir ajouté des actionneurs à l'application bassed Spring Boot, vous disposez d'un /tracepoint de terminaison avec les dernières informations sur les demandes. Ce point de terminaison fonctionne sur la base de TraceRepository et l'implémentation par défaut est InMemoryTraceRepository qui enregistre les 100 derniers appels. Vous pouvez changer cela en implémentant cette interface par vous-même et en la rendant disponible en tant que bean Spring. Par exemple, pour consigner toutes les demandes de consignation (et toujours utiliser l'implémentation par défaut comme stockage de base pour diffuser des informations sur le /tracepoint de terminaison), j'utilise ce type d'implémentation:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.boot.actuate.trace.Trace;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;


@Component
public class LoggingTraceRepository implements TraceRepository {

  private static final Logger LOG = LoggerFactory.getLogger(LoggingTraceRepository.class);
  private final TraceRepository delegate = new InMemoryTraceRepository();

  @Override
  public List<Trace> findAll() {
    return delegate.findAll();
  }

  @Override
  public void add(Map<String, Object> traceInfo) {
    LOG.info(traceInfo.toString());
    this.delegate.add(traceInfo);
  }
}

Cette traceInfocarte contient des informations de base sur demande et la réponse à ce genre de forme: {method=GET, path=/api/hello/John, headers={request={host=localhost:8080, user-agent=curl/7.51.0, accept=*/*}, response={X-Application-Context=application, Content-Type=text/plain;charset=UTF-8, Content-Length=10, Date=Wed, 29 Mar 2017 20:41:21 GMT, status=200}}}. Il n'y a AUCUN contenu de réponse ici.

ÉDITER! Journalisation des données POST

Vous pouvez accéder aux données POST en remplaçant WebRequestTraceFilter , mais ne pensez pas que ce soit une bonne idée (par exemple, tout le contenu du fichier téléchargé ira dans les journaux) Voici un exemple de code, mais ne le faites pas utilisez pas:

package info.fingo.nuntius.acuate.trace;

import org.apache.commons.io.IOUtils;
import org.springframework.boot.actuate.trace.TraceProperties;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;

@Component
public class CustomWebTraceFilter extends WebRequestTraceFilter {

  public CustomWebTraceFilter(TraceRepository repository, TraceProperties properties) {
    super(repository, properties);
}

  @Override
  protected Map<String, Object> getTrace(HttpServletRequest request) {
    Map<String, Object> trace = super.getTrace(request);
    String multipartHeader = request.getHeader("content-type");
    if (multipartHeader != null && multipartHeader.startsWith("multipart/form-data")) {
        Map<String, Object> parts = new LinkedHashMap<>();
        try {
            request.getParts().forEach(
                    part -> {
                        try {
                            parts.put(part.getName(), IOUtils.toString(part.getInputStream(), Charset.forName("UTF-8")));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
            );
        } catch (IOException | ServletException e) {
            e.printStackTrace();
        }
        if (!parts.isEmpty()) {
            trace.put("multipart-content-map", parts);
        }
    }
    return trace;
  }
}
Piotr Chowaniec
la source
1
Et le corps POST?
Pavel Vyazankin
@dart J'ai ajouté un exemple pour vous
Piotr Chowaniec
1
Je faisais quelque chose comme ça, mais le problème est que l'organisme de réponse n'est pas disponible TraceRepository, comment pouvons-nous y accéder?
Amir Pashazadeh
@AmirPashazadeh vous devez remplacer protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)mais je ne sais pas quand ce filtre est exécuté - peut être en phase de demande, donc le corps de réponse ne sera pas prêt là-bas.
Piotr Chowaniec
1
@Kekar ​​Depuis 2.0, il y a HttpTraceRepository (au lieu de TraceRepository)
Piotr Chowaniec
12

Ce code fonctionne pour moi dans une application Spring Boot - il suffit de l'enregistrer en tant que filtre

    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.util.Collection;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Locale;
    import java.util.Map;
    import javax.servlet.*;
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import javax.servlet.http.HttpServletResponse;
    import org.apache.commons.io.output.TeeOutputStream;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;

    @Component
    public class HttpLoggingFilter implements Filter {

        private static final Logger log = LoggerFactory.getLogger(HttpLoggingFilter.class);

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response,
                             FilterChain chain) throws IOException, ServletException {
            try {
                HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;

                Map<String, String> requestMap = this
                        .getTypesafeRequestMap(httpServletRequest);
                BufferedRequestWrapper bufferedRequest = new BufferedRequestWrapper(
                        httpServletRequest);
                BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(
                        httpServletResponse);

                final StringBuilder logMessage = new StringBuilder(
                        "REST Request - ").append("[HTTP METHOD:")
                        .append(httpServletRequest.getMethod())
                        .append("] [PATH INFO:")
                        .append(httpServletRequest.getServletPath())
                        .append("] [REQUEST PARAMETERS:").append(requestMap)
                        .append("] [REQUEST BODY:")
                        .append(bufferedRequest.getRequestBody())
                        .append("] [REMOTE ADDRESS:")
                        .append(httpServletRequest.getRemoteAddr()).append("]");

                chain.doFilter(bufferedRequest, bufferedResponse);
                logMessage.append(" [RESPONSE:")
                        .append(bufferedResponse.getContent()).append("]");
                log.debug(logMessage.toString());
            } catch (Throwable a) {
                log.error(a.getMessage());
            }
        }

        private Map<String, String> getTypesafeRequestMap(HttpServletRequest request) {
            Map<String, String> typesafeRequestMap = new HashMap<String, String>();
            Enumeration<?> requestParamNames = request.getParameterNames();
            while (requestParamNames.hasMoreElements()) {
                String requestParamName = (String) requestParamNames.nextElement();
                String requestParamValue;
                if (requestParamName.equalsIgnoreCase("password")) {
                    requestParamValue = "********";
                } else {
                    requestParamValue = request.getParameter(requestParamName);
                }
                typesafeRequestMap.put(requestParamName, requestParamValue);
            }
            return typesafeRequestMap;
        }

        @Override
        public void destroy() {
        }

        private static final class BufferedRequestWrapper extends
                HttpServletRequestWrapper {

            private ByteArrayInputStream bais = null;
            private ByteArrayOutputStream baos = null;
            private BufferedServletInputStream bsis = null;
            private byte[] buffer = null;

            public BufferedRequestWrapper(HttpServletRequest req)
                    throws IOException {
                super(req);
                // Read InputStream and store its content in a buffer.
                InputStream is = req.getInputStream();
                this.baos = new ByteArrayOutputStream();
                byte buf[] = new byte[1024];
                int read;
                while ((read = is.read(buf)) > 0) {
                    this.baos.write(buf, 0, read);
                }
                this.buffer = this.baos.toByteArray();
            }

            @Override
            public ServletInputStream getInputStream() {
                this.bais = new ByteArrayInputStream(this.buffer);
                this.bsis = new BufferedServletInputStream(this.bais);
                return this.bsis;
            }

            String getRequestBody() throws IOException {
                BufferedReader reader = new BufferedReader(new InputStreamReader(
                        this.getInputStream()));
                String line = null;
                StringBuilder inputBuffer = new StringBuilder();
                do {
                    line = reader.readLine();
                    if (null != line) {
                        inputBuffer.append(line.trim());
                    }
                } while (line != null);
                reader.close();
                return inputBuffer.toString().trim();
            }

        }

        private static final class BufferedServletInputStream extends
                ServletInputStream {

            private ByteArrayInputStream bais;

            public BufferedServletInputStream(ByteArrayInputStream bais) {
                this.bais = bais;
            }

            @Override
            public int available() {
                return this.bais.available();
            }

            @Override
            public int read() {
                return this.bais.read();
            }

            @Override
            public int read(byte[] buf, int off, int len) {
                return this.bais.read(buf, off, len);
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        }

        public class TeeServletOutputStream extends ServletOutputStream {

            private final TeeOutputStream targetStream;

            public TeeServletOutputStream(OutputStream one, OutputStream two) {
                targetStream = new TeeOutputStream(one, two);
            }

            @Override
            public void write(int arg0) throws IOException {
                this.targetStream.write(arg0);
            }

            public void flush() throws IOException {
                super.flush();
                this.targetStream.flush();
            }

            public void close() throws IOException {
                super.close();
                this.targetStream.close();
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {

            }
        }

        public class BufferedResponseWrapper implements HttpServletResponse {

            HttpServletResponse original;
            TeeServletOutputStream tee;
            ByteArrayOutputStream bos;

            public BufferedResponseWrapper(HttpServletResponse response) {
                original = response;
            }

            public String getContent() {
                return bos.toString();
            }

            public PrintWriter getWriter() throws IOException {
                return original.getWriter();
            }

            public ServletOutputStream getOutputStream() throws IOException {
                if (tee == null) {
                    bos = new ByteArrayOutputStream();
                    tee = new TeeServletOutputStream(original.getOutputStream(),
                            bos);
                }
                return tee;

            }

            @Override
            public String getCharacterEncoding() {
                return original.getCharacterEncoding();
            }

            @Override
            public String getContentType() {
                return original.getContentType();
            }

            @Override
            public void setCharacterEncoding(String charset) {
                original.setCharacterEncoding(charset);
            }

            @Override
            public void setContentLength(int len) {
                original.setContentLength(len);
            }

            @Override
            public void setContentLengthLong(long l) {
                original.setContentLengthLong(l);
            }

            @Override
            public void setContentType(String type) {
                original.setContentType(type);
            }

            @Override
            public void setBufferSize(int size) {
                original.setBufferSize(size);
            }

            @Override
            public int getBufferSize() {
                return original.getBufferSize();
            }

            @Override
            public void flushBuffer() throws IOException {
                tee.flush();
            }

            @Override
            public void resetBuffer() {
                original.resetBuffer();
            }

            @Override
            public boolean isCommitted() {
                return original.isCommitted();
            }

            @Override
            public void reset() {
                original.reset();
            }

            @Override
            public void setLocale(Locale loc) {
                original.setLocale(loc);
            }

            @Override
            public Locale getLocale() {
                return original.getLocale();
            }

            @Override
            public void addCookie(Cookie cookie) {
                original.addCookie(cookie);
            }

            @Override
            public boolean containsHeader(String name) {
                return original.containsHeader(name);
            }

            @Override
            public String encodeURL(String url) {
                return original.encodeURL(url);
            }

            @Override
            public String encodeRedirectURL(String url) {
                return original.encodeRedirectURL(url);
            }

            @SuppressWarnings("deprecation")
            @Override
            public String encodeUrl(String url) {
                return original.encodeUrl(url);
            }

            @SuppressWarnings("deprecation")
            @Override
            public String encodeRedirectUrl(String url) {
                return original.encodeRedirectUrl(url);
            }

            @Override
            public void sendError(int sc, String msg) throws IOException {
                original.sendError(sc, msg);
            }

            @Override
            public void sendError(int sc) throws IOException {
                original.sendError(sc);
            }

            @Override
            public void sendRedirect(String location) throws IOException {
                original.sendRedirect(location);
            }

            @Override
            public void setDateHeader(String name, long date) {
                original.setDateHeader(name, date);
            }

            @Override
            public void addDateHeader(String name, long date) {
                original.addDateHeader(name, date);
            }

            @Override
            public void setHeader(String name, String value) {
                original.setHeader(name, value);
            }

            @Override
            public void addHeader(String name, String value) {
                original.addHeader(name, value);
            }

            @Override
            public void setIntHeader(String name, int value) {
                original.setIntHeader(name, value);
            }

            @Override
            public void addIntHeader(String name, int value) {
                original.addIntHeader(name, value);
            }

            @Override
            public void setStatus(int sc) {
                original.setStatus(sc);
            }

            @SuppressWarnings("deprecation")
            @Override
            public void setStatus(int sc, String sm) {
                original.setStatus(sc, sm);
            }

            @Override
            public String getHeader(String arg0) {
                return original.getHeader(arg0);
            }

            @Override
            public Collection<String> getHeaderNames() {
                return original.getHeaderNames();
            }

            @Override
            public Collection<String> getHeaders(String arg0) {
                return original.getHeaders(arg0);
            }

            @Override
            public int getStatus() {
                return original.getStatus();
            }

        }
    }
user1817243
la source
Cela fonctionne bien pour la journalisation des réponses - même si j'ai dû limiter le nombre d'octets qu'il enregistre, sinon il supprime la sortie de la console de journalisation Intellij.
Adam
String getContent () {if (bos == null) {return String.format ("appelé% s trop tôt", BufferedResponseWrapper.class.getCanonicalName ()); } octet [] octets = bos.toByteArray (); return new String (Arrays.copyOf (bytes, 5000)) + "...."; }
Adam
Cela vaut également la peine de mettre également un commutateur "log.isTraceEnabled ()" autour de la journalisation.
Adam
6
Ce qui serait cool, c'est que Java ajoute des méthodes par défaut à HttpServletResponse afin que nous n'ayons pas besoin d'écrire une implémentation aussi énorme.
Adam
1
plus un pour inclure les déclarations d'importation
granadaCoder
8

Actuellement, Spring Boot a la fonction Actuator pour obtenir les journaux des demandes et des réponses.

Mais vous pouvez également obtenir les journaux en utilisant Aspect (AOP).

Aspect vous fournit des annotations telles que : @Before, @AfterReturning, @AfterThrowingetc.

@Beforeconsigne la demande, @AfterReturningconsigne la réponse et@AfterThrowing enregistre le message d'erreur. Vous n'avez peut-être pas besoin du journal de tous les points de terminaison, vous pouvez donc appliquer certains filtres sur les packages.

Voici quelques exemples :

Pour demande:

@Before("within(your.package.where.endpoints.are..*)")
    public void endpointBefore(JoinPoint p) {
        if (log.isTraceEnabled()) {
            log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
            Object[] signatureArgs = p.getArgs();


            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            try {

                if (signatureArgs[0] != null) {
                    log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
                }
            } catch (JsonProcessingException e) {
            }
        }
    }

Ici @Before("within(your.package.where.endpoints.are..*)") le chemin du package. Tous les points de terminaison de ce package généreront le journal.

Pour réponse:

@AfterReturning(value = ("within(your.package.where.endpoints.are..*)"),
            returning = "returnValue")
    public void endpointAfterReturning(JoinPoint p, Object returnValue) {
        if (log.isTraceEnabled()) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            try {
                log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
            } catch (JsonProcessingException e) {
                System.out.println(e.getMessage());
            }
            log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
        }
    }

Voici @AfterReturning("within(your.package.where.endpoints.are..*)")le chemin du package. Tous les points de terminaison de ce package généreront le journal. AussiObject returnValue la réponse.

Par exception:

@AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e")
public void endpointAfterThrowing(JoinPoint p, Exception e) throws DmoneyException {
    if (log.isTraceEnabled()) {
        System.out.println(e.getMessage());

        e.printStackTrace();


        log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
    }
}

Voici @AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e") le chemin du package. Tous les points de terminaison de ce package généreront le journal. AussiException e la réponse d'erreur.

Voici le code complet:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Order(1)
@Component
@ConditionalOnExpression("${endpoint.aspect.enabled:true}")
public class EndpointAspect {
    static Logger log = Logger.getLogger(EndpointAspect.class);

    @Before("within(your.package.where.is.endpoint..*)")
    public void endpointBefore(JoinPoint p) {
        if (log.isTraceEnabled()) {
            log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
            Object[] signatureArgs = p.getArgs();


            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            try {

                if (signatureArgs[0] != null) {
                    log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
                }
            } catch (JsonProcessingException e) {
            }
        }
    }

    @AfterReturning(value = ("within(your.package.where.is.endpoint..*)"),
            returning = "returnValue")
    public void endpointAfterReturning(JoinPoint p, Object returnValue) {
        if (log.isTraceEnabled()) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            try {
                log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
            } catch (JsonProcessingException e) {
                System.out.println(e.getMessage());
            }
            log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
        }
    }


    @AfterThrowing(pointcut = ("within(your.package.where.is.endpoint..*)"), throwing = "e")
    public void endpointAfterThrowing(JoinPoint p, Exception e) throws Exception {
        if (log.isTraceEnabled()) {
            System.out.println(e.getMessage());

            e.printStackTrace();


            log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
        }
    }
}

Ici, en utilisant, @ConditionalOnExpression("${endpoint.aspect.enabled:true}")vous pouvez activer / désactiver le journal. il suffit d'ajouter endpoint.aspect.enabled:truedans leapplication.property et de contrôler le journal

Plus d'informations sur la visite AOP ici:

Quais de printemps sur l'AOP

Exemple d'article sur AOP

Md. Sajedul Karim
la source
1
new ObjectMapper()coûte cher, mieux vaut partager un mappeur pour tous
Sam
Oui bien sûr. Ceci est un code de démonstration. En production, nous devons suivre les meilleures pratiques.
Md. Sajedul Karim
7

Voici ma solution (Spring 2.0.x)

Ajoutez la dépendance maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Modifiez le fichier application.properties et ajoutez la ligne suivante:

management.endpoints.web.exposure.include=* 

Une fois votre application Spring Boot démarrée, vous pouvez suivre les 100 dernières requêtes http en appelant cette URL: http: // localhost: 8070 / actuator / httptrace

Enrico Giurin
la source
5

Vous pouvez également configurer un intercepteur Spring personnalisé HandlerInterceptorAdapterpour une implémentation simplifiée des intercepteurs avant / après seulement:

@Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle (final HttpServletRequest request, final HttpServletResponse response,
            final Object handler)
            throws Exception {

        // Logs here

        return super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler, final Exception ex) {
        // Logs here
    }
}

Ensuite, vous enregistrez autant d'intercepteurs que vous le souhaitez:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    CustomHttpInterceptor customHttpInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(customHttpInterceptor).addPathPatterns("/endpoints");
    }

}

Remarque: comme indiqué par @Robert , vous devez faire attention aux implémentations spécifiques de et que votre application utilise. HttpServletRequestHttpServletResponse

Par exemple, pour les applications utilisant le ShallowEtagHeaderFilter, l'implémentation de la réponse serait un ContentCachingResponseWrapper, vous auriez donc:

@Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomHttpInterceptor.class);

    private static final int MAX_PAYLOAD_LENGTH = 1000;

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler, final Exception ex) {
        final byte[] contentAsByteArray = ((ContentCachingResponseWrapper) response).getContentAsByteArray();

        LOGGER.info("Request body:\n" + getContentAsString(contentAsByteArray, response.getCharacterEncoding()));
    }

    private String getContentAsString(byte[] buf, String charsetName) {
        if (buf == null || buf.length == 0) {
            return "";
        }

        try {
            int length = Math.min(buf.length, MAX_PAYLOAD_LENGTH);

            return new String(buf, 0, length, charsetName);
        } catch (UnsupportedEncodingException ex) {
            return "Unsupported Encoding";
        }
    }

}
bosco
la source
4

@ la réponse de hahn nécessité un peu de modification pour que cela fonctionne pour moi, mais c'est de loin la chose la plus personnalisable que j'ai pu obtenir.

Cela n'a pas fonctionné pour moi, probablement parce que j'ai également un HandlerInterceptorAdapter [??] mais j'ai continué à recevoir une mauvaise réponse du serveur dans cette version. Voici ma modification.

public class LoggableDispatcherServlet extends DispatcherServlet {

    private final Log logger = LogFactory.getLog(getClass());

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

        long startTime = System.currentTimeMillis();
        try {
            super.doDispatch(request, response);
        } finally {
            log(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response),
                    System.currentTimeMillis() - startTime);
        }
    }

    private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, long timeTaken) {
        int status = responseToCache.getStatus();
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("httpStatus", status);
        jsonObject.addProperty("path", requestToCache.getRequestURI());
        jsonObject.addProperty("httpMethod", requestToCache.getMethod());
        jsonObject.addProperty("timeTakenMs", timeTaken);
        jsonObject.addProperty("clientIP", requestToCache.getRemoteAddr());
        if (status > 299) {
            String requestBody = null;
            try {
                requestBody = requestToCache.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
            } catch (IOException e) {
                e.printStackTrace();
            }
            jsonObject.addProperty("requestBody", requestBody);
            jsonObject.addProperty("requestParams", requestToCache.getQueryString());
            jsonObject.addProperty("tokenExpiringHeader",
                    responseToCache.getHeader(ResponseHeaderModifierInterceptor.HEADER_TOKEN_EXPIRING));
        }
        logger.info(jsonObject);
    }
}
AA_PV
la source
votre application est-elle présentée sous forme de guerre ou de bocal? Je reçois toujours l'erreur java.io.FileNotFoundException: impossible d'ouvrir la ressource ServletContext [/WEB-INF/loggingDispatcherServlet-servlet.xml]
Mayank Madhav le
4

Si quelqu'un en a encore besoin, voici une implémentation simple avec Spring HttpTrace Actuator. Mais comme ils l'ont dit plus haut, cela ne fait pas enregistrer les corps.

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.boot.actuate.trace.http.HttpTrace;
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
import org.springframework.stereotype.Repository;

@Slf4j
@Repository
public class LoggingInMemoryHttpTraceRepository extends InMemoryHttpTraceRepository {
    public void add(HttpTrace trace) {
        super.add(trace);
        log.info("Trace:" + ToStringBuilder.reflectionToString(trace));
        log.info("Request:" + ToStringBuilder.reflectionToString(trace.getRequest()));
        log.info("Response:" + ToStringBuilder.reflectionToString(trace.getResponse()));
    }
}
Donz
la source
4

Veuillez vous référer au lien ci-dessous pour la réponse réelle https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba

Quelques modifications ont été apportées à la solution référencée ci-dessus, la demande et la réponse se connecteront également à la console et au fichier si le niveau de l'enregistreur est info. nous pouvons imprimer dans la console ou dans un fichier.

@Component
public class LoggingFilter 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
        );
Logger log = LoggerFactory.getLogger(ReqAndResLoggingFilter.class);
private static final Path path = Paths.get("/home/ramesh/loggerReq.txt");
private static BufferedWriter writer = null;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    try {
        writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"));
    if (isAsyncDispatch(request)) {
        filterChain.doFilter(request, response);
    } else {
        doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
    }
    }finally {
        writer.close();
    }
}

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

protected void beforeRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
    if (log.isInfoEnabled()) {
        logRequestHeader(request, request.getRemoteAddr() + "|>");
    }
}

protected void afterRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
    if (log.isInfoEnabled()) {
        logRequestBody(request, request.getRemoteAddr() + "|>");
        logResponse(response, request.getRemoteAddr() + "|<");
    }
}

private void logRequestHeader(ContentCachingRequestWrapper request, String prefix) throws IOException {
    String queryString = request.getQueryString();
    if (queryString == null) {
        printLines(prefix,request.getMethod(),request.getRequestURI());
        log.info("{} {} {}", prefix, request.getMethod(), request.getRequestURI());
    } else {
        printLines(prefix,request.getMethod(),request.getRequestURI(),queryString);
        log.info("{} {} {}?{}", prefix, request.getMethod(), request.getRequestURI(), queryString);
    }
    Collections.list(request.getHeaderNames()).forEach(headerName ->
    Collections.list(request.getHeaders(headerName)).forEach(headerValue ->
    log.info("{} {}: {}", prefix, headerName, headerValue)));
    printLines(prefix);
    printLines(RequestContextHolder.currentRequestAttributes().getSessionId());
    log.info("{}", prefix);

    log.info(" Session ID: ", RequestContextHolder.currentRequestAttributes().getSessionId());
}

private void printLines(String ...args) throws IOException {

    try {
    for(String varArgs:args) {
            writer.write(varArgs);
            writer.newLine();
    }
        }catch(IOException ex){
            ex.printStackTrace();
    }

}

private void logRequestBody(ContentCachingRequestWrapper request, String prefix) {
    byte[] content = request.getContentAsByteArray();
    if (content.length > 0) {
        logContent(content, request.getContentType(), request.getCharacterEncoding(), prefix);
    }
}

private void logResponse(ContentCachingResponseWrapper response, String prefix) throws IOException {
    int status = response.getStatus();
    printLines(prefix, String.valueOf(status), HttpStatus.valueOf(status).getReasonPhrase());
    log.info("{} {} {}", prefix, status, HttpStatus.valueOf(status).getReasonPhrase());
    response.getHeaderNames().forEach(headerName ->
    response.getHeaders(headerName).forEach(headerValue ->
    log.info("{} {}: {}", prefix, headerName, headerValue)));
    printLines(prefix);
    log.info("{}", prefix);
    byte[] content = response.getContentAsByteArray();
    if (content.length > 0) {
        logContent(content, response.getContentType(), response.getCharacterEncoding(), prefix);
    }
}

private void logContent(byte[] content, String contentType, String contentEncoding, String prefix) {
    MediaType mediaType = MediaType.valueOf(contentType);
    boolean visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
    if (visible) {
        try {
            String contentString = new String(content, contentEncoding);
            Stream.of(contentString.split("\r\n|\r|\n")).forEach(line -> {
                try {
                    printLines(line);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            });
//              log.info("{} {}", prefix, line));
        } catch (UnsupportedEncodingException e) {
            log.info("{} [{} bytes content]", prefix, content.length);
        }
    } else {

        log.info("{} [{} bytes content]", prefix, content.length);
    }
}

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

Sortie dans un fichier:

127.0.0.1|>
POST
/createUser
127.0.0.1|>
session Id:C0793464532E7F0C7154913CBA018B2B
Request:
{
  "name": "asdasdas",
  "birthDate": "2018-06-21T17:11:15.679+0000"
}
127.0.0.1|<
200
OK
127.0.0.1|<
Response:
{"name":"asdasdas","birthDate":"2018-06-21T17:11:15.679+0000","id":4}
Ramesh
la source
1
Excellente réponse, la seule suggestion serait de collecter toutes les sorties dans un tampon et de vous connecter dans une seule instruction.
Mike
2

Si vous ne voyez qu'une partie de la charge utile de votre demande, vous devez appeler la setMaxPayloadLengthfonction car elle affiche par défaut seulement 50 caractères dans votre corps de demande. En outre, la valeur setIncludeHeadersfalse est une bonne idée si vous ne souhaitez pas enregistrer vos en-têtes d'authentification!

@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
    CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
    loggingFilter.setIncludeClientInfo(false);
    loggingFilter.setIncludeQueryString(false);
    loggingFilter.setIncludePayload(true);
    loggingFilter.setIncludeHeaders(false);
    loggingFilter.setMaxPayloadLength(500);
    return loggingFilter;
}
Chris Paika
la source
J'essaie de l'utiliser au printemps mvc et cela ne fonctionne pas pour moi, il faut un paramètre supplémentaire, sauf l'enregistrement de ce bean et l'ajout d'un enregistreur?
Noman Akhtar
1

si vous utilisez Tomcat dans votre application de démarrage, voici org.apache.catalina.filters.RequestDumperFilterun chemin de classe pour vous. (mais il ne vous fournira pas "d'exceptions au même endroit").

Yura
la source
1

le code collé ci-dessous fonctionne avec mes tests et peut être téléchargé depuis mon [projet github] [1], en partageant après avoir appliqué une solution basée sur celle-ci sur un projet de production.

@Configuration
public class LoggingFilter extends GenericFilterBean {

    /**
     * It's important that you actually register your filter this way rather then just annotating it
     * as @Component as you need to be able to set for which "DispatcherType"s to enable the filter
     * (see point *1*)
     * 
     * @return
     */
    @Bean
    public FilterRegistrationBean<LoggingFilter> initFilter() {
        FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LoggingFilter());

        // *1* make sure you sett all dispatcher types if you want the filter to log upon
        registrationBean.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));

        // *2* this should put your filter above any other filter
        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);

        return registrationBean;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        ContentCachingRequestWrapper wreq = 
            new ContentCachingRequestWrapper(
                (HttpServletRequest) request);

        ContentCachingResponseWrapper wres = 
            new ContentCachingResponseWrapper(
                (HttpServletResponse) response);

        try {

            // let it be ...
            chain.doFilter(wreq, wres);

            // makes sure that the input is read (e.g. in 404 it may not be)
            while (wreq.getInputStream().read() >= 0);

            System.out.printf("=== REQUEST%n%s%n=== end request%n",
                    new String(wreq.getContentAsByteArray()));

            // Do whatever logging you wish here, in this case I'm writing request 
            // and response to system out which is probably not what you wish to do
            System.out.printf("=== RESPONSE%n%s%n=== end response%n",
                    new String(wres.getContentAsByteArray()));

            // this is specific of the "ContentCachingResponseWrapper" we are relying on, 
            // make sure you call it after you read the content from the response
            wres.copyBodyToResponse();

            // One more point, in case of redirect this will be called twice! beware to handle that
            // somewhat

        } catch (Throwable t) {
            // Do whatever logging you whish here, too
            // here you should also be logging the error!!!
            throw t;
        }

    }
}
Andrea Saba
la source
0

Afin de consigner toutes les demandes avec des paramètres d'entrée et un corps, nous pouvons utiliser des filtres et des intercepteurs . Mais lors de l'utilisation d'un filtre ou d'un intercepteur, nous ne pouvons pas imprimer le corps de la demande plusieurs fois. La meilleure façon est d'utiliser Spring-AOP. En l'utilisant, nous pouvons dissocier le mécanisme de journalisation de l'application. AOP peut être utilisé pour enregistrer l' entrée et la sortie de chaque méthode dans l'application.

Ma solution est:

 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Pointcut;
 import org.aspectj.lang.reflect.CodeSignature;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 import com.fasterxml.jackson.databind.ObjectMapper;
 @Aspect
 @Component
public class LoggingAdvice {
private static final Logger logger = 
LoggerFactory.getLogger(LoggingAdvice.class);

//here we can provide any methodName, packageName, className 
@Pointcut(value = "execution(* com.package.name.*.*.*(..) )")
public void myPointcut() {

}

@Around("myPointcut()")
public Object applicationLogger(ProceedingJoinPoint pjt) throws Throwable {
    ObjectMapper mapper = new ObjectMapper();
    String methodName = pjt.getSignature().getName();
    String className = pjt.getTarget().getClass().toString();
    String inputParams = this.getInputArgs(pjt ,mapper);
    logger.info("method invoked from " + className + " : " + methodName + "--Request Payload::::"+inputParams);
    Object object = pjt.proceed();
    try {
        logger.info("Response Object---" + mapper.writeValueAsString(object));
    } catch (Exception e) {
    }
    return object;
}

private String getInputArgs(ProceedingJoinPoint pjt,ObjectMapper mapper) {
    Object[] array = pjt.getArgs();
    CodeSignature signature = (CodeSignature) pjt.getSignature();

    StringBuilder sb = new StringBuilder();
    sb.append("{");
    int i = 0;
    String[] parameterNames = signature.getParameterNames();
    int maxArgs = parameterNames.length;
    for (String name : signature.getParameterNames()) {
        sb.append("[").append(name).append(":");
        try {
            sb.append(mapper.writeValueAsString(array[i])).append("]");
            if(i != maxArgs -1 ) {
                sb.append(",");
            }
        } catch (Exception e) {
            sb.append("],");
        }
        i++;
    }
    return sb.append("}").toString();
}

}

Rushikesh Kulkarni
la source
0

Si vous avez configuré le serveur Spring Boot Config, il vous suffit d'activer l'enregistreur de débogage pour la classe:

Http11InputBuffer.Http11InputBuffer.java

Les débogages consignent toutes les demandes et réponses pour chaque demande

Narendra
la source
-1

Pour enregistrer les demandes qui aboutissent à 400 uniquement:

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

import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.AbstractRequestLoggingFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;

/**
 * Implementation is partially copied from {@link AbstractRequestLoggingFilter} and modified to output request information only if request resulted in 400.
 * Unfortunately {@link AbstractRequestLoggingFilter} is not smart enough to expose {@link HttpServletResponse} value in afterRequest() method.
 */
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {

    public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";

    public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";

    private final boolean includeQueryString = true;
    private final boolean includeClientInfo = true;
    private final boolean includeHeaders = true;
    private final boolean includePayload = true;

    private final int maxPayloadLength = (int) (2 * FileUtils.ONE_MB);

    private final String afterMessagePrefix = DEFAULT_AFTER_MESSAGE_PREFIX;

    private final String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;

    /**
     * The default value is "false" so that the filter may log a "before" message
     * at the start of request processing and an "after" message at the end from
     * when the last asynchronously dispatched thread is exiting.
     */
    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
            throws ServletException, IOException {

        final boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestToUse = request;

        if (includePayload && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
            requestToUse = new ContentCachingRequestWrapper(request, maxPayloadLength);
        }

        final boolean shouldLog = shouldLog(requestToUse);

        try {
            filterChain.doFilter(requestToUse, response);
        } finally {
            if (shouldLog && !isAsyncStarted(requestToUse)) {
                afterRequest(requestToUse, response, getAfterMessage(requestToUse));
            }
        }
    }

    private String getAfterMessage(final HttpServletRequest request) {
        return createMessage(request, this.afterMessagePrefix, this.afterMessageSuffix);
    }

    private String createMessage(final HttpServletRequest request, final String prefix, final String suffix) {
        final StringBuilder msg = new StringBuilder();
        msg.append(prefix);
        msg.append("uri=").append(request.getRequestURI());

        if (includeQueryString) {
            final String queryString = request.getQueryString();
            if (queryString != null) {
                msg.append('?').append(queryString);
            }
        }

        if (includeClientInfo) {
            final String client = request.getRemoteAddr();
            if (StringUtils.hasLength(client)) {
                msg.append(";client=").append(client);
            }
            final HttpSession session = request.getSession(false);
            if (session != null) {
                msg.append(";session=").append(session.getId());
            }
            final String user = request.getRemoteUser();
            if (user != null) {
                msg.append(";user=").append(user);
            }
        }

        if (includeHeaders) {
            msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
        }

        if (includeHeaders) {
            final ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
            if (wrapper != null) {
                final byte[] buf = wrapper.getContentAsByteArray();
                if (buf.length > 0) {
                    final int length = Math.min(buf.length, maxPayloadLength);
                    String payload;
                    try {
                        payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
                    } catch (final UnsupportedEncodingException ex) {
                        payload = "[unknown]";
                    }
                    msg.append(";payload=").append(payload);
                }
            }
        }
        msg.append(suffix);
        return msg.toString();
    }

    private boolean shouldLog(final HttpServletRequest request) {
        return true;
    }

    private void afterRequest(final HttpServletRequest request, final HttpServletResponse response, final String message) {
        if (response.getStatus() == HttpStatus.BAD_REQUEST.value()) {
            logger.warn(message);
        }
    }

}
Ihor M.
la source