Every program needs to record business logs, so there are log libraries in various languages, such as Log2j in Java. There are also many options in Node.js, such as Winston, Log4JS, Bunyan, etc. Winston is easy to use and supports multiple transport channels.

The basic use

const winston = require('winston')
winston.log('info'.'Hello World! ')
winston.info('Hello World')
Copy the code

Logs are printed to the console by default. We can also create multiple instances in the following way:

const logger1 = winston.createLogger()
const logger2 = winston.createLogger()
logger1.info('logger1')
logger2.info('logger2')
Copy the code

Transmission channel

When Winston receives the log, he sends it as a message to a different transport channel, our usual console print and file storage.

Built-in transmission channel

Most of the time you want to both receive logs on the console and save them to a file, which is very simple in Winston:

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({filename: 'combined.log'})
  ]
})
logger.info('console and file')
Copy the code

In this case, the console also records the output in the combined. Log file. Winston has 4 channels by default:

  • Console: Print to the Console
  • File: Records to a File
  • Http: Transmits data over Http
  • Stream: transmits data through a Stream

The following code demonstrates the use of all of the above built-in channels:

// Create a writable stream
const {Writable} = require('stream')
const stream = new Writable({
  objectMode: false.write: raw= > console.log('stream msg', raw.toString())
})
// Create the HTTP service
const http = require('http')
http
  .createServer((req, res) = > {
    const arr = []
    req
      .on('data'.chunk= > arr.push(chunk))
      .on('end'.() = > {
        const msg = Buffer.concat(arr).toString()
        console.log('http msg', msg)
        res.end(msg)
      })
  })
  .listen(8080)
// Configure four channels
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({filename: 'combined.log'}),
    new winston.transports.Http({host: 'localhost'.port: 8080}),
    new winston.transports.Stream({stream})
  ]
})
// Transfer to channel
logger.info('winston transports')
Copy the code

The console, file, HTTP server, and writable stream all receive the following message:

{"message":"winston transports","level":"info"}
Copy the code

Custom transport channel

The built-in channels are already quite powerful, but if you don’t feel like it’s enough, you can write your own channel, such as to MongoDB or Kafka or ElasticSearch.

class CustomTransport extends winston.Transport {
  constructor(opts) {
    super(opts)
  }

  log(info, callback) {
    console.log('info',info)
    callback()
  }
}
Copy the code

As long as you write a class that inherits from Winston.Transport, Winston receives the log and triggers the execution of the class’s log method. The argument is the message object that contains the log.

  • inconstructorConstructor to create remote connections (MongoDB, Kafka, ElasticSearch…)
  • inlogMethod to process and send messages

formatting

By default, Winston logs are generated in JSON format. The log content is in the message field. JSON contains other fields, such as level. Winston also has a number of built-in formatting tools, such as:

const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.label({ label: 'right meow! ' }),
    winston.format.timestamp(),
    winston.format.prettyPrint(),
  ),
  transports: [new winston.transports.Console()]
})
logger.info('hello world')
Copy the code

Will output:

{
  message: 'hello world'.level: 'info'.label: 'right meow! '.timestamp: 'the 2020-08-28 T06: and thou. 836 z'
}
Copy the code

We have complete control over the format of the log, for example

const customFormat = winston.format.printf((info) = > {
  return `[do whatever you want] ${info.timestamp}:${info.label}:${info.message}`
})
Copy the code

You just write a function, the arguments are Winston’s message objects, you do what you want with the function, and you just return the formatted string.

Log cutting

If all logs are written to a file, the file will become very large after a long time, and it is very troublesome to process. At this time, log cutting is required. There are two common cutting methods:

  • Cut by file size
  • Cut by write time

Cut by size

Just add the maxsize parameter when creating the file channel, for example:

const maxsizeTransport = new winston.transports.File({
  level: 'info'.format: winston.format.printf(info= > info.message),
  filename: path.join(__dirname, '.. '.'logs'.'testmaxsize.log'),
  maxsize: 1024
})
Copy the code

Testmaxsize1.log, testMaxSize2.log, and so on are created when the number of files exceeds 1024.

Cut by time

There is an official time cutting library called Winston-daily-rotate File, which can be cut by day:

new transports.DailyRotateFile({
  filename: path.join(__dirname, '.. '.'logs'.`%DATE%.log`),
  datePattern: 'YYYY-MM-DD'.prepend: true.json: false
})
Copy the code

2020-01-01.log, 2020-01-02.log, and so on.

Dynamic multi-instance

As your application grows, you may need to configure different logs for different functional domains. For example, you may need to log orders and logon to different files, and you may need to store a copy of orders to ElasticSearch.

winston.loggers.add('order', {format: orderFormat, transports: orderTransports})
winston.loggers.add('login', {format: loginFormat, transports: loginTransports})
const orderLog = winston.loggers.get('order')
const loginLog = winston.loggers.get('login')
orderLog.info('Order Log')
loginLog.error('Login error')
Copy the code

You can create a default instance when initializing the project, and then dynamically add domain instances as the business grows:

if(! winston.loggers.has('xxx')) {
  winston.loggers.add('xxx', {format: xxxFormat, transports: xxxTransports})
}
Copy the code