
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; }
Niveau | Journal |
---|
TRACE DEBUG | INFO [] rteeExample.verbose > s="s", i=4, d=5.6 INFO [] rteeExample.verbose < false |
INFO | INFO [] 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; }
Niveau | Journal |
---|
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) ..
|
ERROR | ERROR [] 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) { }
Niveau | Journal |
---|
TRACE | INFO [] rteeExample.parameterLevels > d=9.4, s="v", i=7 |
DEBUG | INFO [] rteeExample.parameterLevels > d=9.4, s="v" |
INFO | INFO [] 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; }
Niveau | Journal |
---|
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); }
Niveau | Journal |
---|
TRACE DEBUG | DEBUG [] rteeExample.manual > INFO [] rteeExample.manual - Eager logging: 3.141592653589793 DEBUG [] rteeExample.manual - Lazy logging: 3.141592653589793 DEBUG [] rteeExample.manual < |
INFO | INFO [] 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);
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.