Eclair - Bibliothèque de journalisation déclarative Java Spring



Il y a beaucoup de questions sur le travail des services aux stades de développement, de test et de support, et tous sont à première vue différents: "Que s'est-il passé?" , "Y avait-il une demande?" , "Quel est le format de date?" , "Pourquoi le service ne répond-il pas?" etc.

Un journal correctement compilé pourra répondre en détail à ces questions et à bien d'autres de manière absolument autonome sans la participation des développeurs. Dans la poursuite d'un objectif aussi tentant, la bibliothèque de journalisation Eclair est née, conçue pour engager un dialogue avec tous les participants au processus sans tirer trop de couvertures.

À propos de la couverture et des fonctionnalités de la solution - ci-dessous.

Quel est le problème de la journalisation


Si vous n'êtes pas très intéressé par la compréhension des lieux, vous pouvez immédiatement procéder à la description de notre solution .

  • Le journal des applications est son alibi.
    Le plus souvent, lui seul peut prouver le succès de la candidature. Il n'y a pas d'état dans un microservice; les systèmes adjacents sont mobiles et capricieux. «Répéter», «recréer», «revérifier» - tout cela est difficile et / ou impossible. Le journal doit être suffisamment informatif pour répondre à la question: «Que s'est-il passé?» À tout moment . . Le journal doit être clair pour tout le monde: le développeur, le testeur, parfois l'analyste, parfois l'administrateur, parfois la première ligne de support - tout se passe.
  • Les microservices concernent le multithreading.
    Les demandes qui arrivent au service (ou les données demandées par le service) sont le plus souvent traitées par plusieurs threads. Le journal de tous les threads est généralement mélangé. Voulez-vous faire la distinction entre les threads parallèles et distinguer les threads "séquentiels"? Le même flux est réutilisé pour le traitement séquentiel des demandes, exécutant encore et encore sa propre logique pour différents ensembles de données. Ces flux "séquentiels" proviennent d'un autre plan, mais leurs limites doivent être claires pour le lecteur.
  • Le journal doit enregistrer le format de données d'origine.
    Si en réalité les services sont échangés par XML, le journal correspondant doit stocker XML. Ce n'est pas toujours compact et pas toujours beau (mais pratique). Il est plus facile de voir le succès, d’analyser l’échec plus facilement. Dans certains cas, le journal peut être utilisé pour lire ou retraiter manuellement la demande.
  • Une partie des données du journal nécessite une relation spéciale.
    Les données entrantes (demandes), les données sortantes (réponses), les demandes adressées à des systèmes tiers et leurs réponses doivent souvent être stockées séparément. Ils sont soumis à des exigences particulières: par durée de vie ou fiabilité. De plus, ces données peuvent avoir une quantité impressionnante par rapport à une ligne de journal typique.
  • Une partie des données n'est pas destinée au journal.
    Les éléments suivants doivent généralement être exclus du journal normal: données binaires (tableaux d'octets, base64, ..), données personnelles des clients / partenaires / autres personnes physiques et morales. C'est toujours une histoire individuelle, mais systématique et ne se prête pas à un contrôle manuel.

Pourquoi pas les mains


Prenez org.slf4j.Logger ( org.slf4j.Logger vous avec les annexes de n'importe quelle couleur) et écrivez tout ce qui est nécessaire dans le journal. Les entrées aux principales méthodes, les sorties, si nécessaire, reflètent les erreurs détectées, certaines données. Est-ce nécessaire? Oui, mais:

  • La quantité de code augmente de manière déraisonnable (inhabituelle). Au début, ce n'est pas très frappant, si vous vous connectez uniquement aux plus basiques (support réussi, soit dit en passant, avec cette approche).
  • Appeler l'enregistreur avec vos mains devient rapidement de la paresse. Déclarer un champ static avec un enregistreur est trop paresseux (enfin, Lombok peut le faire pour nous). Nous, les développeurs, sommes paresseux. Et nous écoutons notre paresse, c'est une noble paresse: elle change constamment le monde pour le mieux.
  • Les microservices ne sont pas bons de tous les côtés. Oui, elles sont petites et jolies, mais il y a un revers: il y en a beaucoup! Une seule application du début à la fin est souvent écrite par un développeur. L'héritage n'apparaît pas sous ses yeux. Heureux, pas chargé de règles imposées, le développeur considère qu'il est du devoir d'inventer son propre format de log, son principe et ses propres règles. Puis, met en œuvre avec brio l'invention. Chaque classe est différente. Est-ce un problème? Colossal.
  • La refactorisation cassera votre journal. Même l'idée omnipotente ne le sauvera pas. La mise à jour du journal est aussi impossible que la mise à jour de Javadoc. Dans le même temps, au moins Javadoc est lu uniquement par les développeurs (non, personne ne lit), mais l'audience des journaux est beaucoup plus large et l'équipe de développement n'est pas limitée.
  • MDC (Mapped Diagnostic Context) fait partie intégrante d'une application multithread. Le remplissage manuel du MDC nécessite un nettoyage en temps opportun à la fin des travaux dans le flux. Sinon, vous courez le risque de lier un ThreadLocal à des données non liées. Les mains et les yeux pour contrôler cela, j'ose dire, est impossible.

Et c'est ainsi que nous résolvons ces problèmes dans nos applications.

Qu'est-ce qu'Eclair et que peut-il faire


Eclair est un outil qui simplifie l'écriture de code enregistré. Il permet de collecter les méta-informations nécessaires sur le code source, de les associer aux données volantes dans l'application lors de l'exécution et de les envoyer au référentiel de journaux habituel, tout en générant un minimum de code.

L'objectif principal est de rendre le journal compréhensible pour tous les participants au processus de développement. Par conséquent, la commodité de l'écriture de code, les avantages d'Eclair ne s'arrêtent pas, mais commencent seulement.

Eclair enregistre les méthodes et paramètres annotés:

  • enregistre l'entrée / sortie de la méthode de la méthode / exceptions / arguments / valeurs renvoyées par la méthode
  • filtre les exceptions pour les enregistrer spécifiquement dans les types: uniquement lorsque cela est nécessaire
  • fait varier le "détail" du journal, en fonction des paramètres de l'application pour l'emplacement actuel: par exemple, dans le cas le plus détaillé, il imprime les valeurs des arguments (tout ou partie), dans la version la plus courte - uniquement le fait d'entrer la méthode
  • imprime les données au format JSON / XML / dans tout autre format (prêt à travailler avec Jackson, JAXB prêt à l'emploi): comprend quel format est le plus préférable pour un paramètre particulier
  • comprend SpEL (Spring Expression Language) pour l'installation déclarative et l'auto-nettoyage MDC
  • écrit dans N loggers, le "logger" dans la compréhension d'Eclair est un bean dans le contexte qui implémente l'interface EclairLogger : vous pouvez spécifier le logger qui doit traiter l'annotation par nom, par alias, ou par défaut
  • informe le programmeur de certaines erreurs dans l'utilisation des annotations: par exemple, Eclair sait qu'il fonctionne sur des proxys dynamiques (avec toutes les fonctionnalités qui en découlent), par conséquent, il peut suggérer que l'annotation sur la méthode private ne fonctionnera jamais
  • accepte les méta annotations (comme les appelle Spring): vous pouvez définir vos annotations pour la journalisation, en utilisant quelques annotations de base - pour réduire le code
  • capable de masquer les données «sensibles» lors de l'impression: prêt à l'emploi XPath-shielding XML
  • écrit un journal en mode "manuel", définit l'invocateur et "développe" les arguments qui implémentent le Supplier : donnant la possibilité d'initialiser les arguments "paresseusement"

Comment connecter Eclair


Le code source est publié sur GitHub sous la licence Apache 2.0.

Pour vous connecter, vous avez besoin de Java 8, Maven et Spring Boot 1.5+. Artefact hébergé par Maven Central Repository:

 <dependency> <groupId>ru.tinkoff</groupId> <artifactId>eclair-spring-boot-starter</artifactId> <version>0.8.3</version> </dependency> 

Le démarreur contient une implémentation standard d' EclairLogger , qui utilise un système de journalisation initialisé par Spring Boot avec un ensemble de paramètres vérifiés.

Des exemples


Voici quelques exemples d'utilisation typique d'une bibliothèque. D'abord, un fragment de code est donné, puis le journal correspondant, selon la disponibilité d'un certain niveau de journalisation. Un ensemble d'exemples plus complet peut être trouvé sur le wiki du projet dans la section Exemples .

Exemple le plus simple


Le niveau par défaut est DEBUG.

 @Log void simple() { } 
Si le niveau est disponible... alors le journal sera comme ça
TRACE
DEBUG
DEBUG [] rteeExample.simple >
DEBUG [] rteeExample.simple <
INFO
WARN
ERROR
-

Les détails du journal dépendent du niveau de journalisation disponible.


Le niveau de journalisation disponible à l'emplacement actuel affecte les détails du journal. Plus le niveau disponible est bas (c'est-à-dire plus proche de TRACE), plus le journal est détaillé.

 @Log(INFO) boolean verbose(String s, Integer i, Double d) { return false; } 
NiveauJournal
TRACE
DEBUG
INFO [] rteeExample.verbose > s="s", i=4, d=5.6
INFO [] rteeExample.verbose < false
INFOINFO [] rteeExample.verbose >
INFO [] rteeExample.verbose <
WARN
ERROR
-

Journalisation des exceptions de réglage fin


Les types d'exceptions enregistrées peuvent être filtrés. Les exceptions sélectionnées et leurs descendants seront promis. Dans cet exemple, NullPointerException sera enregistré au niveau WARN, Exception au niveau ERROR (par défaut) et Error ne sera pas enregistré du tout (car Error pas inclus dans le filtre de la première annotation @Log.error et est explicitement exclu du filtre de la deuxième annotation).

 @Log.error(level = WARN, ofType = {NullPointerException.class, IndexOutOfBoundsException.class}) @Log.error(exclude = Error.class) void filterErrors(Throwable throwable) throws Throwable { throw throwable; } //       filterErrors(new NullPointerException()); filterErrors(new Exception()); filterErrors(new Error()); 
NiveauJournal
TRACE
DEBUG
INFO
WARN
WARN [] rteeExample.filterErrors ! java.lang.NullPointerException
java.lang.NullPointerException: null
at rteeExampleTest.filterErrors(ExampleTest.java:0)
..
ERROR [] rteeExample.filterErrors ! java.lang.Exception
java.lang.Exception: null
at rteeExampleTest.filterErrors(ExampleTest.java:0)
..
ERRORERROR [] rteeExample.filterErrors ! java.lang.Exception
java.lang.Exception: null
at rteeExampleTest.filterErrors(ExampleTest.java:0)
..

Réglez chaque paramètre séparément


 @Log.in(INFO) void parameterLevels(@Log(INFO) Double d, @Log(DEBUG) String s, @Log(TRACE) Integer i) { } 
NiveauJournal
TRACEINFO [] rteeExample.parameterLevels > d=9.4, s="v", i=7
DEBUGINFO [] rteeExample.parameterLevels > d=9.4, s="v"
INFOINFO [] rteeExample.parameterLevels > 9.4
WARN
ERROR
-

Sélectionnez et personnalisez le format d'impression


Les «imprimantes» responsables du format d'impression peuvent être configurées par des pré et post-processeurs. Dans l'exemple ci-dessus, maskJaxb2Printer configuré de sorte que les éléments correspondant à l'expression XPath "//s" sont masqués à l'aide de "********" . Dans le même temps, jacksonPrinter imprime Dto "tel jacksonPrinter ".

 @Log.out(printer = "maskJaxb2Printer") Dto printers(@Log(printer = "maskJaxb2Printer") Dto xml, @Log(printer = "jacksonPrinter") Dto json, Integer i) { return xml; } 
NiveauJournal
TRACE
DEBUG
DEBUG [] rteeExample.printers >
xml=<dto><i>5</i><s>********</s></dto>, json={"i":5,"s":"password"}
DEBUG [] rteeExample.printers <
<dto><i>5</i><s>********</s></dto>
INFO
WARN
ERROR
-

Plusieurs enregistreurs en contexte


La méthode est enregistrée à l'aide de plusieurs enregistreurs en même temps: par défaut enregistreur (annoté à l'aide de @Primary ) et auditLogger. Vous pouvez définir plusieurs enregistreurs si vous souhaitez séparer les événements enregistrés non seulement par niveau (TRACE - ERREUR), mais aussi les envoyer à différents stockages. Par exemple, l'enregistreur principal peut écrire un journal dans un fichier sur le disque à l'aide de slf4j, et auditLogger peut écrire une tranche de données spéciale dans un excellent stockage (par exemple, dans Kafka) dans son propre format spécifique.

 @Log @Log(logger = "auditLogger") void twoLoggers() { } 

Gestion MDC


Les MDC définis à l'aide d'annotations sont automatiquement supprimés après avoir quitté la méthode annotée. Une valeur d'enregistrement MDC peut être calculée dynamiquement à l'aide de SpEL. Voici des exemples: une chaîne statique perçue par une constante, évaluant l'expression 1 + 1 , appelant le jacksonPrinter , appelant la méthode static randomUUID .
Les MDC avec l'attribut global = true ne sont pas supprimés après avoir quitté la méthode: comme vous pouvez le voir, le seul enregistrement restant dans le MDC jusqu'à la fin du journal est sum .

 @Log void outer() { self.mdc(); } @Mdc(key = "static", value = "string") @Mdc(key = "sum", value = "1 + 1", global = true) @Mdc(key = "beanReference", value = "@jacksonPrinter.print(new ru.tinkoff.eclair.example.Dto())") @Mdc(key = "staticMethod", value = "T(java.util.UUID).randomUUID()") @Log void mdc() { self.inner(); } @Log.in void inner() { } 

Connectez-vous lors de l'exécution du code ci-dessus:
DEBUG [] rteeExample.outer >
DEBUG [beanReference={"i":0,"s":null}, sum=2, static=string, staticMethod=01234567-89ab-cdef-ghij-klmnopqrstuv] rteeExample.mdc >
DEBUG [beanReference={"i":0,"s":null}, sum=2, static=string, staticMethod=01234567-89ab-cdef-ghij-klmnopqrstuv] rteeExample.inner >
DEBUG [beanReference={"i":0,"s":null}, sum=2, static=string, staticMethod=01234567-89ab-cdef-ghij-klmnopqrstuv] rteeExample.mdc <
DEBUG [sum=2] rteeExample.outer <


Installation MDC basée sur des paramètres


Si vous spécifiez le MDC à l'aide de l'annotation sur le paramètre, le paramètre annoté est disponible en tant qu'objet racine du contexte d'évaluation. Ici "s" est un champ de classe Dto de type String .

 @Log.in void mdcByArgument(@Mdc(key = "dto", value = "#this") @Mdc(key = "length", value = "s.length()") Dto dto) { } 

Connectez-vous lors de l'exécution du code ci-dessus:
DEBUG [length=8, dto=Dto{i=12, s='password'}] rteeExample.mdcByArgument > dto=Dto{i=12, s='password'}

Journalisation manuelle


Pour une journalisation "manuelle", il suffit d'implémenter l'implémentation de ManualLogger . Les arguments passés qui implémentent le Supplier interface ne seront "développés" que si nécessaire.

 @Autowired private ManualLogger logger; @Log void manual() { logger.info("Eager logging: {}", Math.PI); logger.debug("Lazy logging: {}", (Supplier) () -> Math.PI); } 
NiveauJournal
TRACE
DEBUG
DEBUG [] rteeExample.manual >
INFO [] rteeExample.manual - Eager logging: 3.141592653589793
DEBUG [] rteeExample.manual - Lazy logging: 3.141592653589793
DEBUG [] rteeExample.manual <
INFOINFO [] rteeExample.manual - Eager logging: 3.141592653589793
WARN
ERROR
-

Qu'est-ce que Eclair ne fait pas


Eclair ne sait pas où vous stockerez vos journaux, pour combien de temps et en détail. Eclair ne sait pas comment vous prévoyez d'utiliser votre journal. Eclair extrait soigneusement de votre application toutes les informations dont vous avez besoin et les redirige vers le stockage que vous avez configuré.

Un exemple de configuration d' EclairLogger dirigeant un journal vers un enregistreur Logback avec un Appender spécifique:

 @Bean public EclairLogger eclairLogger() { LoggerFacadeFactory factory = loggerName -> { ch.qos.logback.classic.LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); ch.qos.logback.classic.Logger logger = context.getLogger(loggerName); // Appender<ILoggingEvent> appender = ? // logger.addAppender(appender); return new Slf4JLoggerFacade(logger); }; return new SimpleLogger(factory, LoggingSystem.get(SimpleLogger.class.getClassLoader())); } 

Cette solution n'est pas pour tout le monde.


Avant de commencer à utiliser Eclair comme outil principal de journalisation, vous devez vous familiariser avec un certain nombre de fonctionnalités de cette solution. Ces «fonctionnalités» sont dues au fait qu'Eclair est basé sur le mécanisme de proxy standard pour Spring.

- La vitesse d'exécution du code encapsulé dans le prochain proxy est insignifiante, mais elle chutera. Pour nous, ces pertes sont rarement importantes. Si la question se pose de réduire le délai de livraison, il existe de nombreuses mesures d'optimisation efficaces. Le refus d'un journal informatif pratique peut être considéré comme l'une des mesures, mais pas en premier lieu.

- StackTrace "gonfle" un peu plus. Si vous n'êtes pas habitué à la longue pile de traces de proxys Spring, cela peut être une nuisance pour vous. Pour une raison tout aussi évidente, le débogage des classes mandataires sera difficile.

- Toutes les classes et toutes private méthodes ne peuvent pas être proxy : private méthodes private ne peuvent pas être proxy, vous aurez besoin de vous-même pour enregistrer la chaîne de méthodes dans un bean, vous ne pouvez pas proxy tout ce qui n'est pas un bean, etc.

En fin de compte


Il est clair que cet outil, comme tout autre, doit pouvoir être utilisé pour en bénéficier. Et ce matériau n'illumine que superficiellement le côté dans lequel nous avons décidé de nous déplacer à la recherche de la solution parfaite.

Critique, réflexions, astuces, liens - je salue chaleureusement toute participation à la vie du projet! Je serais ravi que vous trouviez Eclair utile pour vos projets.

Source: https://habr.com/ru/post/fr412871/


All Articles