class-logger
Boilerplate-free decorator-based class logging. Log method calls and creation of your class easily with the help of two decorators. No prototype mutation. Highly configurable. Built with TypeScript. Works with Node.js and in browser.
@LogClass()
class Test {
@Log()
method1() {
return 123
}
}
Logs Test.construct. Args: [].
before a class instance is created.
Logs Test.method1. Args: [].
before the method call.
Logs Test.method1 -> done. Args: []. Res: 123.
after it.
Installation
-
Run
npm i class-logger reflect-metadata
-
If you use TypeScript set in you tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
-
If you use JavaScript configure your babel to support decorators and class properties
-
At the top of your project root file add
import 'reflect-metadata'
Requirements
Your environment must support Proxy. For Node.js it's 6.4.0+, for browsers it's Edge 12+, Firefox 18+, Chrome 49+, Safari 10+.
You can log:
- Class construction
- Prototype and static method calls, both: synchronous and asynchronous. Any thrown errors are properly logged and re-thrown.
- Own and static property calls if those properties return functions (synchronous or asynchronous). Error handling is the same as for method calls.
import { LogClass, Log } from 'class-logger'
@LogClass()
class Test {
@Log()
method1() {
return 123
}
@Log()
async methodAsync1() {
return Symbol()
}
@Log()
methodError() {
throw new Error()
}
@Log()
property1 = () => null
@Log()
static methodStatic1(arg1) {
return {
prop1: 'test',
}
}
}
Test.methodStatic1(42)
const test = new Test()
test.method1()
test.methodAsync1()
test.methodError()
test.property1()
Configuration
Configuration object
Here's how the configuration object looks like:
interface IClassLoggerConfig {
log?: (message: string) => void
logError?: (message: string) => void
formatter?: {
start: (data: IClassLoggerFormatterStartData) => string
end: (data: IClassLoggerFormatterEndData) => string
}
include?: {
args:
| boolean
| {
start: boolean
end: boolean
}
construct: boolean
result: boolean
classInstance:
| boolean
| {
start: boolean
end: boolean
}
}
}
There're 3 layers of config:
Every time class-logger
logs a message all 3 of them are merged together.
Global config
You can set it using setConfig
function from class-logger
.
import { setConfig } from 'class-logger'
setConfig({
})
Class config
You can set it using LogClass
decorator from class-logger
.
import { LogClass } from 'class-logger'
LogClass({
})
class Test {}
Method config
You can set it using Log
decorator from class-logger
.
import { Log } from 'class-logger'
LogClass()
class Test {
@Log({
})
method1() {}
}
Include
classInstance
It enables/disabled including the formatted class instance to your log messages. But what does 'formatted' really mean here? So if you decide to include it (remember, it's false
by default), default class formatter (ClassLoggerFormatterService
) is going to execute this sequence:
- Take own (non-prototype) properties of an instance.
- Why? It's a rare case when your prototype changes dynamically, therefore it hardly makes any sense to log it.
- Drop any of them that have
function
type.
- Why? Most of the time
function
properties are just immutable arrow functions used instead of regular class methods to preserve this
context. It doesn't make much sense to bloat your logs with stringified bodies of those functions.
- Transform any of them that are not plain objects recursively.
- What objects are plain ones?
ClassLoggerFormatterService
considers an object a plain object if its prototype is strictly equal to Object.prototype
. - Why? Often we include instances of other classes as properties (inject them as dependencies). By stringifying them using the same algorithm we can see what we injected.
- Stringify what's left.
Example:
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
}
}
const test = new Test()
test.method2()
If a class instance is not available at the moment (e.g. for class construction or calls of static methods), it logs N/A
.
Examples
Disable logging of arguments for all messages
{
include: {
args: false
}
}
Disable logging of arguments for end messages
{
include: {
args: {
start: true
end: false
}
}
}
Enable logging of a formatted class instance for all messages
{
include: {
classInstance: true
}
}
Enable logging of a formatted class instance for end messages
{
include: {
classInstance: {
start: true
end: false
}
}
}
Disable logging of class construction
{
include: {
construct: false
}
}
Disable logging of method's return value (or thrown error)
{
include: {
result: false
}
}
Change logger
{
log: myLogger.debug,
logError: myLogger.error
}
Which could look like this in real world:
import { setConfig } from 'class-logger'
import { createLogger } from 'winston'
const logger = createLogger()
setConfig({
log: logger.debug.bind(logger),
logError: logger.error.bind(logger),
})
Formatting
You can pass your own custom formatter to the config to format messages to your liking.
{
formatter: myCustomFormatter
}
Your custom formatter must be an object with properties start
and end
. It must comply with the following interface:
interface IClassLoggerFormatter {
start: (data: IClassLoggerFormatterStartData) => string
end: (data: IClassLoggerFormatterEndData) => string
}
where IClassLoggerFormatterStartData
is:
interface IClassLoggerFormatterStartData {
args: any[]
className: string
propertyName: string | symbol
classInstance?: any
include: {
args:
| boolean
| {
start: boolean
end: boolean
}
construct: boolean
result: boolean
classInstance:
| boolean
| {
start: boolean
end: boolean
}
}
}
and IClassLoggerFormatterEndData
is:
interface IClassLoggerFormatterEndData {
args: any[]
className: string
propertyName: string | symbol
classInstance?: any
result: any
error: boolean
include: {
args:
| boolean
| {
start: boolean
end: boolean
}
construct: boolean
result: boolean
classInstance:
| boolean
| {
start: boolean
end: boolean
}
}
}
You can provide your own object with these two properties, but the easiest way to modify the formatting logic of class-logger
is to subclass the default formatter - ClassLoggerFormatterService
.
ClassLoggerFormatterService
has these protected
methods which are building blocks of final messages:
base
operation
args
classInstance
result
final
Generally speaking, start
method of ClassLoggerFormatterService
is base
+ args
+ classInstance
+ final
. end
is base
+ operation
+ args
+ classInstance
+ result
+ final
.
Examples
Let's take a look at how we could add a timestamp to the beginning of each message:
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(),
})
FYI, winston, pino and pretty much any other logger are capable of adding timestamps on their own, so this example is purely educative. I'd advice to use your logger's built-in mechanism for creating timestamps if possible.