Eclair - Biblioteca de registro declarativo Java Spring



Hay muchas preguntas sobre el trabajo de los servicios en las etapas de desarrollo, prueba y soporte, y todas son, a primera vista, diferentes de: "¿Qué pasó?" , "¿Hubo una solicitud?" , "¿Cuál es el formato de fecha?" , "¿Por qué el servicio no responde?" etc.

Un registro correctamente compilado podrá responder en detalle estas y muchas otras preguntas de forma absolutamente autónoma sin la participación de los desarrolladores. En la búsqueda de un objetivo tan tentador, nació la biblioteca de registro Eclair, diseñada para entablar un diálogo con todos los participantes en el proceso sin tirar demasiadas mantas.

Sobre la manta y las características de la solución, a continuación.

¿Cuál es el problema del registro?


Si no está muy interesado en comprender las premisas, puede proceder inmediatamente a la descripción de nuestra solución .

  • El registro de la aplicación es su coartada.
    Muy a menudo, solo él puede demostrar el éxito de la aplicación. No hay estado en un microservicio; los sistemas adyacentes son móviles y delicados. “Repetir”, “recrear”, “verificar dos veces”: todo esto es difícil y / o imposible. El registro debe ser lo suficientemente informativo como para responder la pregunta: "¿Qué pasó?" En cualquier momento . . El registro debe ser claro para todos: el desarrollador, el probador, a veces el analista, a veces el administrador, a veces la primera línea de soporte, sucede cualquier cosa.
  • Los microservicios se tratan de subprocesos múltiples.
    Las solicitudes que llegan al servicio (o los datos solicitados por el servicio) se procesan con mayor frecuencia por varios subprocesos. El registro de todos los hilos suele ser mixto. ¿Desea distinguir entre hilos paralelos y distinguir entre hilos "secuenciales"? El mismo flujo se reutiliza para el procesamiento secuencial de solicitudes, ejecutando una y otra vez su propia lógica para diferentes conjuntos de datos. Estos flujos "secuenciales" desde otro plano, pero sus límites deben ser claros para el lector.
  • El registro debe guardar el formato de datos original.
    Si en realidad los servicios son intercambiados por XML, entonces el registro correspondiente debe almacenar XML. No siempre es compacto y no siempre es hermoso (pero conveniente). Es más fácil ver el éxito, más fácil analizar el fracaso. En algunos casos, el registro se puede usar para reproducir o reprocesar manualmente la solicitud.
  • Parte de los datos en el registro requiere una relación especial.
    Los datos entrantes (solicitudes), los datos salientes (respuestas), las solicitudes a sistemas de terceros y las respuestas de ellos a menudo deben almacenarse por separado. Están sujetos a requisitos especiales: por vida útil o confiabilidad. Además, estos datos pueden tener una cantidad impresionante en comparación con una línea de registro típica.
  • Parte de los datos no es para el registro.
    Por lo general, lo siguiente debe excluirse del registro regular: datos binarios (conjuntos de bytes, base64, ..), datos personales de clientes / socios / otras personas y entidades jurídicas. Siempre es una historia individual, pero sistemática y no se presta al control manual.

¿Por qué no las manos?


Tome org.slf4j.Logger ( org.slf4j.Logger a iniciar sesión con Appenders de cualquier palo) y escriba todo lo que se requiere en el registro. Las entradas a los métodos principales, salidas, si es necesario, reflejan errores detectados, algunos datos. ¿Es esto necesario? Sí, pero:

  • La cantidad de código está creciendo de manera irrazonable (inusualmente). Al principio, esto no es muy sorprendente, si registra solo los más básicos (soporte exitoso, por cierto, con este enfoque).
  • Llamar al registrador con las manos rápidamente se convierte en pereza. Declarar un campo static con un registrador es demasiado vago (bueno, Lombok puede hacer esto por nosotros). Los desarrolladores somos flojos. Y escuchamos nuestra pereza, esto es noble pereza: está cambiando constantemente el mundo para mejor.
  • Los microservicios no son buenos en todos los lados. Sí, son pequeños y bonitos, pero hay un lado negativo: ¡hay muchos! Una sola aplicación de principio a fin a menudo es escrita por un desarrollador. El legado no se cierne ante sus ojos. Feliz, no cargado de reglas impuestas, el desarrollador considera que es un deber inventar su propio formato de registro, su principio y sus propias reglas. Entonces, implementa brillantemente la invención. Cada clase es diferente. ¿Es esto un problema? Colosal.
  • Refactorizar romperá su registro. Incluso la idea omnipotente no lo salvará. Actualizar el registro es tan imposible como actualizar el Javadoc. Al mismo tiempo, al menos Javadoc es leído solo por los desarrolladores (no, nadie lee), pero la audiencia de registros es mucho más amplia y el equipo de desarrollo no está limitado.
  • MDC (Mapped Diagnostic Context) es una parte integral de una aplicación multiproceso. El llenado manual del MDC requiere una limpieza oportuna al final del trabajo en la secuencia. De lo contrario, corre el riesgo de vincular un ThreadLocal a datos no relacionados. Las manos y los ojos para controlar esto, me atrevo a decir, es imposible.

Y así es como resolvemos estos problemas en nuestras aplicaciones.

¿Qué es Eclair y qué puede hacer?


Eclair es una herramienta que simplifica la escritura del código registrado. Ayuda a recopilar la metainformación necesaria sobre el código fuente, asociarla con los datos que vuelan en la aplicación en tiempo de ejecución y enviarla al repositorio de registro habitual, mientras genera un mínimo de código.

El objetivo principal es hacer que el registro sea comprensible para todos los participantes en el proceso de desarrollo. Por lo tanto, la conveniencia de escribir código, los beneficios de Eclair no terminan, sino que solo comienzan.

Eclair registra métodos y parámetros anotados:

  • registra la entrada / salida del método del método / excepciones / argumentos / valores devueltos por el método
  • filtra las excepciones para registrarlas específicamente en tipos: solo cuando sea necesario
  • varía el "detalle" del registro, en función de la configuración de la aplicación para la ubicación actual: por ejemplo, en el caso más detallado, imprime los valores de los argumentos (todos o algunos), en la versión más corta, solo el hecho de ingresar el método
  • imprime datos como JSON / XML / en cualquier otro formato (listo para trabajar con Jackson, JAXB fuera de la caja): comprende qué formato es más preferible para un parámetro en particular
  • entiende el SpEL (Spring Expression Language) para la instalación declarativa y la limpieza automática de MDC
  • escribe en N loggers, el "logger" en la comprensión de Eclair es un bean en el contexto que implementa la interfaz EclairLogger : puede especificar el registrador que debe procesar la anotación por nombre, por alias o por defecto
  • le informa al programador acerca de algunos errores al usar anotaciones: por ejemplo, Eclair sabe que funciona en proxies dinámicos (con todas las funciones que le siguen), por lo tanto, puede sugerir que la anotación en el método private nunca funcionará
  • acepta meta anotaciones (como Spring las llama): puede definir sus anotaciones para iniciar sesión, utilizando algunas anotaciones básicas, para reducir el código
  • capaz de enmascarar datos "confidenciales" al imprimir: fuera de la caja XPath-shielding XML
  • escribe un registro en el modo "manual", define el invocador y "expande" los argumentos que implementan el Supplier : dando la oportunidad de inicializar los argumentos "perezosamente"

Cómo conectar Eclair


El código fuente se publica en GitHub bajo la licencia Apache 2.0.

Para conectarse, necesita Java 8, Maven y Spring Boot 1.5+. Artefacto alojado por el depósito central de Maven:

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

El iniciador contiene una implementación estándar de EclairLogger , que utiliza un sistema de registro inicializado por Spring Boot con algún conjunto verificado de configuraciones.

Ejemplos


Aquí hay algunos ejemplos del uso típico de la biblioteca. Primero, se proporciona un fragmento de código, luego el registro correspondiente, dependiendo de la disponibilidad de un cierto nivel de registro. Se puede encontrar un conjunto más completo de ejemplos en el Wiki del proyecto en la sección Ejemplos .

Ejemplo más simple


El nivel predeterminado es DEBUG.

 @Log void simple() { } 
Si el nivel está disponible... entonces el registro será así
TRACE
DEBUG
DEBUG [] rteeExample.simple >
DEBUG [] rteeExample.simple <
INFO
WARN
ERROR
-

Los detalles del registro dependen del nivel de registro disponible.


El nivel de registro disponible en la ubicación actual afecta los detalles del registro. Cuanto menor sea el nivel disponible (es decir, cuanto más cerca esté de TRACE), más detallado será el registro.

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

Ajuste fino del registro de excepciones


Se pueden filtrar los tipos de excepciones registradas. Las excepciones seleccionadas y sus descendientes se comprometerán. En este ejemplo, NullPointerException se registrará en el nivel WARN, Exception en el nivel ERROR (por defecto), y Error no se registrará en absoluto (porque el Error no Error incluido en el filtro de la primera anotación @Log.error y está explícitamente excluido del filtro de la segunda anotación).

 @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()); 
NivelRegistro
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)
..

Establecer cada parámetro por separado


 @Log.in(INFO) void parameterLevels(@Log(INFO) Double d, @Log(DEBUG) String s, @Log(TRACE) Integer i) { } 
NivelRegistro
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
-

Seleccionar y personalizar el formato de impresión


Las "impresoras" responsables del formato de impresión pueden configurarse mediante preprocesadores y postprocesadores. En el ejemplo anterior, maskJaxb2Printer configurado para que los elementos que coinciden con la expresión XPath "//s" se enmascaren con "********" . Al mismo tiempo, jacksonPrinter imprime Dto "tal cual".

 @Log.out(printer = "maskJaxb2Printer") Dto printers(@Log(printer = "maskJaxb2Printer") Dto xml, @Log(printer = "jacksonPrinter") Dto json, Integer i) { return xml; } 
NivelRegistro
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
-

Múltiples registradores en contexto


El método se registra utilizando varios registradores al mismo tiempo: registrador predeterminado (anotado con @Primary ) y auditLogger auditLogger. Puede definir varios registradores si desea separar los eventos registrados no solo por nivel (TRACE - ERROR), sino también enviarlos a diferentes almacenes. Por ejemplo, el registrador principal puede escribir un registro en un archivo en el disco usando slf4j, y auditLogger puede escribir un segmento de datos especial en un excelente almacenamiento (por ejemplo, en Kafka) en su propio formato específico.

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

MDC Management


Los MDC configurados con anotaciones se eliminan automáticamente después de salir del método anotado. Un valor de registro MDC se puede calcular dinámicamente usando SpEL. Los siguientes son ejemplos: una cadena estática percibida por una constante, evaluando la expresión 1 + 1 , llamando al jacksonPrinter , llamando al método static randomUUID .
Los MDC con el atributo global = true no se eliminan después de salir del método: como puede ver, el único registro que queda en el MDC hasta el final del registro es la 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() { } 

Inicie sesión al ejecutar el código anterior:
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 <


Instalación de MDC basada en parámetros


Si especifica el MDC utilizando la anotación en el parámetro, el parámetro anotado está disponible como el objeto raíz del contexto de evaluación. Aquí "s" es un campo de clase Dto con tipo String .

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

Inicie sesión al ejecutar el código anterior:
DEBUG [length=8, dto=Dto{i=12, s='password'}] rteeExample.mdcByArgument > dto=Dto{i=12, s='password'}

Registro manual


Para el registro "manual", es suficiente implementar la implementación de ManualLogger . Los argumentos aprobados que implementan el Supplier interfaz se "expandirán" solo si es necesario.

 @Autowired private ManualLogger logger; @Log void manual() { logger.info("Eager logging: {}", Math.PI); logger.debug("Lazy logging: {}", (Supplier) () -> Math.PI); } 
NivelRegistro
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é no hace Eclair?


Eclair no sabe dónde almacenará sus registros, por cuánto tiempo y en detalle. Eclair no sabe cómo planea usar su registro. Eclair extrae cuidadosamente de su aplicación toda la información que necesita y la redirige al almacenamiento que configuró.

Un ejemplo de configuración de EclairLogger dirige un registro a un registrador Logback con un Appender específico:

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

Esta solución no es para todos.


Antes de comenzar a usar Eclair como la herramienta principal para iniciar sesión, debe familiarizarse con una serie de características de esta solución. Estas "características" se deben al hecho de que Eclair se basa en el mecanismo proxy estándar para Spring.

- La velocidad de ejecución del código envuelto en el próximo proxy es insignificante, pero caerá. Para nosotros, estas pérdidas rara vez son significativas. Si surge la pregunta de reducir el tiempo de entrega, existen muchas medidas de optimización efectivas. Rechazar un registro informativo conveniente puede considerarse como una de las medidas, pero no en primer lugar.

- StackTrace "hincha" un poco más. Si no estás acostumbrado a la larga pila de Trax de Spring, esto puede ser una molestia para ti. Por una razón igualmente obvia, la depuración de las clases proxy será difícil.

- No todas las clases y todos los métodos pueden ser representados : private métodos private no pueden ser representados, necesitará registrar la cadena de métodos en un bean, no puede representar cualquier cosa que no sea un bean, etc.

Al final


Está completamente claro que esta herramienta, como cualquier otra, debe poder utilizarse para beneficiarse de ella. Y este material solo ilumina superficialmente el lado en el que decidimos movernos en busca de la solución perfecta.

Críticas, pensamientos, sugerencias, enlaces: ¡acojo con beneplácito su participación en la vida del proyecto! Me encantaría que Eclair sea útil para sus proyectos.

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


All Articles