
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; }
Nivel | Registro |
---|
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 | - |
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; }
Nivel | Registro |
---|
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) .. |
Establecer cada parámetro por separado
@Log.in(INFO) void parameterLevels(@Log(INFO) Double d, @Log(DEBUG) String s, @Log(TRACE) Integer i) { }
Nivel | Registro |
---|
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 | - |
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; }
Nivel | Registro |
---|
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); }
Nivel | Registro |
---|
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é 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);
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.