Eclair - Java Spring Declarative Logging Library



There are a lot of questions about the work of services at the stages of development, testing and support, and they all seem different at first glance: “What happened?” , “Was there a request?” , “What date format?” , “Why does the service not respond?” And t .d

Correctly compiled log will be able to answer these and many other questions completely autonomously without the participation of developers. In the pursuit of such a tempting goal, the Eclair logging library was born, designed to carry on a dialogue with all participants in the process, without pulling too much blanket over it.

On the blanket and features of the solution - on.

What is the problem logging


If you are not very interested in understanding the prerequisites, you can go directly to the description of our solution .


Why not hands


Take org.slf4j.Logger (Logback with Appender to it for any suit) and write to the log everything you need. Inputs to the main methods, outputs, if necessary, reflect the errors caught, some data. Is this necessary? Yes, but:


And this is how we solve these problems in our applications.

What is Eclair, and what can it do


Eclair is a tool that simplifies the writing of logged code. It helps to collect the necessary meta-information about the source code, associate it with the data flying in the application at runtime and send it to the log repository you are accustomed to, generating at least a minimum of code.

The main goal is to make the log understandable to all participants of the development process. Therefore, the convenience of Eclair does not end with the convenience of writing code, it is just beginning.

Eclair logs annotated methods and parameters:


How to connect Eclair


The source code is published in GitHub under the Apache 2.0 license.

To connect, you need Java 8, Maven and Spring Boot 1.5+. The artifact is located in the Maven Central Repository:

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

The starter contains a standard implementation of EclairLogger , which uses an initialized logging system with a spring boot with some verified set of settings.

Examples


Here are some examples of typical library use. First, a code fragment is given, then the corresponding log, depending on the availability of a certain level of logging. A more complete set of examples can be found on the Project Wiki in the Examples section.

The simplest example


The default level is DEBUG.

 @Log void simple() { } 
If available level... then the log will be like this
TRACE
DEBUG
DEBUG [] rteeExample.simple >
DEBUG [] rteeExample.simple <
INFO
WARN
ERROR
-

Log detail depends on available logging level.


The logging level available in the current location affects the log details. The “lower” the available level (i.e., the closer to TRACE), the more detailed the log.

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

Fine tuning logging exceptions


Types of logged exceptions can be filtered. Selected exceptions and their descendants will be logged. In this example, NullPointerException will be logged at the WARN level, Exception at the ERROR level (by default), and Error will not be logged at all (because Error not included in the filter of the first @Log.error annotation and is explicitly excluded from the filter of the second 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()); 
LevelLog
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)
..

Configure each parameter separately


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

Select and customize the print format


“Printers” responsible for the print format can be configured by pre- and post-processors. In the given example, maskJaxb2Printer configured so that the elements corresponding to the XPath expression "//s" are masked with the help of "********" . At the same time, jacksonPrinter prints Dto "as is".

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

Several loggers in context


The method is logged using several loggers at the same time: the default logger (annotated with @Primary ) and the auditLogger auditLogger . You can define several loggers if you want to divide logged events not only by levels (TRACE - ERROR), but also send them to different storages. For example, the main logger can write a log to a file to disk using slf4j, and auditLogger can write a special data slice to an excellent repository (for example, Kafka) in its specific format.

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

MDC Management


MDCs set using annotation are automatically deleted after leaving the annotated method. The value of an entry in MDC can be calculated dynamically using SpEL. In the example, the static string perceived by the constant, the calculation of the expression 1 + 1 , the call to the jacksonPrinter , the static call of the randomUUID method are randomUUID .
MDCs with the global = true attribute are not deleted after exiting the method: as you can see, the only entry left in the MDC until the end of the log is the 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() { } 

Log when executing the above code:
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 <


MDC installation based on parameters


If you specify MDC using annotation on a parameter, the annotated parameter is available as the root of the evaluation context. Here "s" is a Dto class field of type String .

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

Log when executing the above code:
DEBUG [length=8, dto=Dto{i=12, s='password'}] rteeExample.mdcByArgument > dto=Dto{i=12, s='password'}

Manual logging


For “manual” logging, it is enough to implement the ManualLogger implementation. Transmitted arguments that implement interface Supplier will be “deployed” only when necessary.

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

What the Eclair doesn't do


Eclair does not know where you will keep your logs, how long and detailed it is. Eclair does not know how you plan to use your log. Eclair gently pulls all the information you need from your application and redirects it to the repository you have configured.

An example of an EclairLogger configuration that sends a log to a Logback logger with a specific Appender:

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

This solution is not for everyone.


Before you start using Eclair as the main logging tool, you should be familiar with a number of features of this solution. These "features" are due to the fact that the Eclair is based on the standard proxying mechanism for Spring.

- The speed of execution of the code wrapped in the next proxy is insignificant, but it will fall. For us, these losses are rarely significant. If the question arises about reducing the execution time, there are many effective optimization measures. Refusal of a convenient informative log can be considered as one of measures, but not first of all.

- StackTrace "swells up" a little more. If you are not used to long stacktracks from Spring proxies, this can be a nuisance to you. For an equally obvious reason, it is difficult to debug proxied classes.

- Not every class and not every method can be proxied : private methods cannot be proxied, for logging the chain of methods in one bean you will need self, you can proxy anything that is not a bean, etc.

At last


It is perfectly clear that this tool, like any other, needs to be able to apply in order to benefit from it. And this material only superficially illuminates the side to which we decided to move in search of the ideal solution.

Criticism, thoughts, hints, links - all your participation in the life of the project, I warmly welcome! I would be glad if you find Eclair useful for your projects.

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


All Articles