📜 ⬆️ ⬇️

Javascript logging is super simple - two decorators and ready


Are you not bothered to write logger.info('ServiceName.methodName.') And logger.info('ServiceName.methodName -> done.') For each? Maybe you, like me, have repeatedly thought about automating this business? In this article, the story will focus on class-logger , as one of the solutions to the problem with just two decorators.


Why is it even necessary?


Not all perfectionists are engineers, but all engineers are perfectionists (well, almost). We like beautiful laconic abstractions. We see beauty in a set of cracks that a person unprepared and cannot read. We like to feel like gods, creating micro-universes that live by our rules. Presumably, all these qualities originate from our immense laziness. No, the engineer is not afraid of work as a class, but he hates the routine repetitive actions that the hands are trying to automate.


Having written several thousand lines of code intended only for logging, we usually arrive at the creation of some of our own patterns and standards for writing messages. Unfortunately, we still have to apply these patterns manually. The most important "why" class-logger is to provide a declarative, configurable way to easily log template messages when creating a class and calling its methods.


Checkers bald!


Without wandering around the bush, let's go straight to the code.


 import { LogClass, Log } from 'class-logger' @LogClass() class ServiceCats { @Log() eat(food: string) { return 'purr' } } 

This service will create three entries in the log:



In words of code:


 //    ServiceCats // `ServiceCats.construct. Args: [].` const serviceCats = new ServiceCats() //    `eat` // `ServiceCats.eat. Args: [milk].` serviceCats.eat('milk') //    `eat` // `ServiceCats.eat -> done. Args: [milk]. Res: purr.` 

Poke "live" .


A complete list of events that can be logged:



A functional property is a switch function assigned to a property ( class ServiceCats { private meow = () => null } ). Most often used to save the execution context ( this ).

We were promised a "configurable" logging method


class-logger three levels of configuration hierarchy:



When calling each method, all three levels merge together. The library provides a sensible global config by default, so it can be used without any prior configuration.


Global config


It is created for the entire application. You can override it with setConfig .


 import { setConfig } from 'class-logger' setConfig({ log: console.info, }) 

Class config


It is its own for each class and is used for all methods of this class. Can redefine global config.


 import { LogClass } from 'class-logger' setConfig({ log: console.info, }) @LogClass({ //       log: console.debug, }) class ServiceCats {} 

Method config


It works only for the method itself. It takes precedence over the class config and the global config.


 import { LogClass } from 'class-logger' setConfig({ log: console.info, }) @LogClass({ //       log: console.debug, }) class ServiceCats { private energy = 100 @Log({ //        log: console.warn, }) eat(food: string) { return 'purr' } //    `console.debug`    sleep() { this.energy += 100 } } 

Poke "live"


What can be configured?


We considered how to change the config, but still have not figured out exactly what can be changed in it.


Here you can find many examples of configuration objects for different cases .

An interface reference if you understand TypeScript better. than russian :)

The configuration object has the following properties:


log


This is a function that deals, in fact, with logging. She comes to the input formatted messages as strings. Used to log the following events:



The default is console.log .


logError


This is a function that also deals with logging, but only error messages. She also comes to the input formatted messages as strings. Used to log the following events:



The default is console.error .


formatter


This is an object with two methods: start and end . These methods create that very formatted message.


start creates messages for the following events:



end creates messages for the following events:



The default is new ClassLoggerFormatterService() .


include


Config of what should be included in the final message.


args

This can be either a boolean value or an object.


If it is boolean , then it sets whether to include a list of arguments (remember Args: [milk] ?) In all messages ( start and end ).


If it is an object, then it should have two boolean properties: start and end . start specifies whether to include the argument list for start messages, end for end messages.


The default is true .


construct

This is a boolean that controls whether to create a class or not.


The default is true .


result

This is a boolean that controls the logged value returned from the method. It is worth noting that the return value is not only what the method returns when it is successfully executed, but also an error that it throws in case of unsuccessful execution. Remember Res: purr ? If this flag is false , then there will be no Res: purr .


The default is true .


classInstance

This can be either a boolean value or an object. The concept is the same as include.args .


Adds a serialized view of your class instance to messages. In other words, if your class has any properties, they will be serialized in JSON and added to the logs.


Not all properties are serializable. The class-logger uses the following logic:



 class ServiceA {} @LogClass({ include: { classInstance: true, }, }) class Test { private serviceA = new ServiceA() private prop1 = 42 private prop2 = { test: 42 } private method1 = () => null @Log() public method2() { return 42 } } //    `Test` // 'Test.construct. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}.' const test = new Test() //    `method2` // 'Test.method2. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}.' test.method2() //    `method2` // 'Test.method2 -> done. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}. Res: 42.' 

The default is false .


We take full control over the formatting of messages


What to do if you like the idea, but your idea of ​​the beautiful insists on a different format of message lines? You can take complete control over the formatting of messages by transferring your own formatter.


You can write your own formatter from scratch. Of course. But this option will not be covered under this (if you are interested in this option, take a look at the "Formatting" section in the README).


The quickest and easiest way to redefine formatting is to inherit your formatter from the default formatter - ClassLoggerFormatterService .


ClassLoggerFormatterService has the following protected methods that create small blocks of final messages:



The start message consists of:



The end message consists of:



You can override only the base methods you need.


Let's take a look at how you can add a timestamp to all messages.


I'm not saying that this should be done in real projects. pino , winston and most other loggers can do this on their own. This example is for educational purposes only.

 import { ClassLoggerFormatterService, IClassLoggerFormatterStartData, setConfig, } from 'class-logger' class ClassLoggerTimestampFormatterService extends ClassLoggerFormatterService { protected base(data: IClassLoggerFormatterStartData) { const baseSuper = super.base(data) const timestamp = Date.now() const baseWithTimestamp = `${timestamp}:${baseSuper}` return baseWithTimestamp } } setConfig({ formatter: new ClassLoggerTimestampFormatterService(), }) 

Poke "live"


Conclusion


Remember to follow the installation instructions and review the requirements before using this library on your project.


I hope you did not waste your time, and the article was just a little useful to you. Please kick and criticize. We will learn to code better together.


')

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


All Articles