@infotorg/winston-log-formats
v2.0.4
Published
Infotorg log formats for Winston logger
Downloads
220
Readme
@infotorg/winston-log-formats
Infotorg log formats for Winston logger.
Formats are fully compatible with Winston and could be combined with other formats.
Installation
npm install @infotorg/winston-log-formats
Usage
The order of combining formats is important. Formats are applying in the same order as they passed into
combine
function. See example below.
const { format } = require('logform');
// All custom Infotorg Log formats for Winston
const { axios, description, filter, mask, requestId, trackId } = require('@infotorg/winston-log-formats');
const { MASK_DATA_SEVERITY_PARTIAL } = require('@infotorg/mask-data-severity-levels');
const infotorgFormat = format.combine(
axios({
enabled: true,
meta: true,
headers: true,
stack: true,
}),
description({ description: 'Your default description' }),
// Configuration for the "filter" format
filter({
target: 'meta',
blackList: [
'req.headers.common',
'req.headers.delete',
'req.headers.get',
// ...
],
}),
// Configuration for the "mask" format
mask({
severity: MASK_DATA_SEVERITY_PARTIAL,
target: 'meta',
// More details about maskOptions you can find in the https://github.com/coderua/mask-data#default-options.
maskOptions: {
// To limit the output String length to 30.
maxMaskedChars: 30,
// First 2 symbols that won't be masked
unmaskedStartChars: 2,
// Last 2 symbols that won't be masked
unmaskedEndChars: 2,
// Do not mask data with type 'boolean'
maskBoolean: false,
// Do not mask 'undefined' data
maskUndefined: false,
// Do not mask 'null' data
maskNull: false,
// Do not mask 'number' data
maskNumber: false,
},
whiteList: [
'res.status',
'res.method',
// Headers
'res.headers.accept-ranges',
'res.headers.x-powered-by',
'res.headers.vary',
'res.statusText',
// Errors related fields
'res.errno',
'res.code',
'res.stack',
'res.data.error',
// ...
'req.timeout',
'req.httpVersion',
'req.originalUrl',
'req.baseURL',
'req.url',
'req.method',
// ...
],
}),
trackId({ trackId: (info) => 'Your track ID or requests ID', key: 'trackId' }),
// Other finalizing formats...
format.json()
);
Axios
The axios
format performs the following actions with the info
object
- Detect if the
message
property is an Axios Request/Response object or if info itself an Axios Error. - Transform Axios Request/Response and Error objects.
- Create a
meta
information depends on a result or error. - Add response time to the log message if
requestStartedAt
property is set. Which can be done by using axios interceptors. - Adds a result to the
meta
property (or any other that is set in themetaKey
option).
It accepts the following options:
- enabled: Enable/disable axios format output. If set to
false
then it just pass through the info object and do nothing. Default value istrue
. - requestDescription: Description for a Request. Default value is
Axios request
. - responseDescription: Description for a Response. Default value is
Axios response
. - errorDescription: Description for an Error. Default value is
Axios error
. - meta: Enable/disable including meta information about request/response/error. Default value is
false
. - metaKey: Key name for meta property. Default value is
meta
. - headers: If true, then request/response headers will be included in the meta. Default value is
true
. - stack: If true, then error stack trace will be included in the meta. Default value is
false
.
IMPORTANT! It should be applied as one of the first Infotorg custom formats in the combine pipeline. Because it creates a proper structure for the next formats. Before
filter
,mask
and other finalizing formats likeerrors
andjson
.
Most convenient using this format together with axios interceptors. See example below.
const axios = require('axios');
axios.interceptors.request.use(
function (config) {
// Add time when a request is started
if (!config.requestStartedAt) {
config.requestStartedAt = new Date().getTime();
}
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
Format Axios Request only
const { axios } = require('@infotorg/winston-log-formats');
// Simulate Axios Request Config
// It will be cought by axios interceptor and sent to logger
const request = {
url: '/api/login',
method: 'post',
data: { name: 'John', password: 'super-secret-passwd' },
headers: {
common: { Accept: 'application/json, text/plain, */*' },
'Content-Type': 'application/json',
'User-Agent': 'axios/1.3.3',
'Content-Length': 49,
},
baseURL: 'https://example.com',
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
requestStartedAt: 1676736693452,
};
// Transform Axios Request to a log message
const info = axios().transform(
// Log entry
{
level: 'info',
message: request,
},
// Options
{ meta: true }
);
console.log(info);
// { level: 'info',
// message: 'POST https://example.com/api/login',
// description: 'Axios request',
// meta: {
// req: {
// baseURL: 'https://example.com',
// data: {
// name: 'John',
// password: 'super-secret-passwd',
// },
// headers: {
// common: { Accept: 'application/json, text/plain, */*' },
// 'Content-type': 'application/json',
// 'User-Agent': 'axios/1.3.3',
// 'Content-Length': 49,
// },
// method: 'post',
// url: '/api/login',
// timeout: '0',
// requestStartedAt: 1676736693452
// }
// }
// }
Format Axios Request and Response
const { axios } = require('@infotorg/winston-log-formats');
// Simulate Axios Response
// It will be cought by axios interceptor and sent to logger
const response = {
status: 200,
statusText: 'OK',
headers: {
'content-length': '3513',
'content-type': 'application/json',
date: 'Sat Feb 18 2023 17:16:25 GMT',
connection: 'close',
},
// Axios Request config
config: {
url: '/api/login',
method: 'post',
data: { name: 'John', password: 'super-secret-passwd' },
headers: {
common: { Accept: 'application/json, text/plain, */*' },
'Content-Type': 'application/json',
'User-Agent': 'axios/1.3.3',
'Content-Length': 49,
},
baseURL: 'https://example.com',
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
requestStartedAt: 1676736693452,
},
responseTime: 100,
};
// Transform Axios Response to log message
const info = axios().transform(
// Log entry
{
level: 'info',
message: response,
},
// Options
{ meta: true }
);
console.log(info);
// { level: 'info',
// message: 'POST https://example.com/api/login 200 OK 100ms',
// description: 'Axios reponse',
// meta: {
// req: {
// baseURL: 'https://example.com',
// data: { name: 'John', password: 'super-secret-passwd' },
// headers: {
// Accept: 'application/json, text/plain, */*',
// 'Content-Length': 49,
// 'Content-Type': 'application/json',
// 'User-Agent': 'axios/1.3.3',
// },
// method: 'post',
// requestStartedAt,
// url: '/api/login',
// xsrfCookieName: 'XSRF-TOKEN',
// xsrfHeaderName: 'X-XSRF-TOKEN',
// },
// res: {
// headers: {
// 'content-length': '2',
// 'content-type': 'application/json',
// },
// status: 200,
// statusText: 'OK',
// responseTime: 100
// }
// }
// }
Format Axios Network Error (ECONNREFUSED, ECONNRESET, ENOTFOUND, etc.)
Network errors are errors that occur when an underlying operation fails due to a network error. These errors are usually generated by the operating system and are usually related to the network stack. This kind of errors won't have a response object. That's why we add error information to
meta.res
key to keep the same structure as for response logs.
ECONNREFUSED
(Connection refused): No connection could be made because the target machine actively refused it. This usually results from trying to connect to a service that is inactive on the foreign host.ECONNRESET
(Connection reset by peer): A connection was forcibly closed by a peer. This normally results from a loss of the connection on the remote socket due to a timeout or reboot. Commonly encountered via the http and net modules.ENOTFOUND
(DNS lookup failed): Indicates a DNS failure of eitherEAI_NODATA
orEAI_NONAME
. This is not a standard POSIX error.
For examples bellow we will use a helper to generate Axios Error. No need to use this in the real world it's a part of axios.
// Helper for creation an axios error
function createAxiosError(message, config, code, request, response) {
const error = new Error(message);
error.config = config;
if (code) {
error.code = code;
}
error.request = request;
error.response = response;
error.isAxiosError = true;
error.toJSON = function toJSON() {
return {
// Standard
message: this.message,
name: this.name,
// Microsoft
description: this.description,
number: this.number,
// Mozilla
fileName: this.fileName,
lineNumber: this.lineNumber,
columnNumber: this.columnNumber,
stack: this.stack,
// Axios
config: this.config,
code: this.code,
status: this.response && this.response.status ? this.response.status : null,
};
};
return error;
}
const { axios } = require('@infotorg/winston-log-formats');
// Simulate creating an Axios Network error
const message = 'connect ECONNREFUSED 127.0.0.1:3333';
const code = 'ECONNREFUSED';
const config = {
headers: {
Accept: 'application/json, text/plain, */*',
'User-Agent': 'axios/1.3.3',
},
method: 'get',
url: 'http://localhost:3333',
requestStartedAt,
data: undefined,
};
const request = {
method: 'GET',
path: '/',
host: 'localhost',
protocol: 'http:',
};
// This Axios Network Error that will be cought by axios interceptor and sent to logger
const axiosError = Object.assign(createAxiosError(message, config, code, request), {
level: 'error',
errno: -61,
code,
syscall: 'connect',
hostname: 'localhost',
address: '127.0.0.1',
port: 3333,
});
// Transform Axios Network Error to log message
const error = axios().transform(
// Error instance (log entry)
axiosError,
// Options
{ meta: true, stack: true }
);
console.log(error);
// { level: 'error',
// message: 'connect ECONNREFUSED 127.0.0.1:3333',
// meta:
// { res:
// { errno: -61,
// code: 'ECONNREFUSED',
// syscall: 'connect',
// hostname: 'localhost',
// address: '127.0.0.1',
// port: 3333,
// stack: 'Error: connect ECONNREFUSED 127.0.0.1:3333\n at ...',
// status: null },
// req:
// { headers:
// { Accept: 'application/json, text/plain, */*',
// 'User-Agent': 'axios/1.3.3' },
// method: 'get',
// url: 'http://localhost:3333',
// requestStartedAt: 1675770391875,
// data: undefined } },
// description: 'Axios error',
// [Symbol(level)]: 'error',
// [Symbol(message)]: 'connect ECONNREFUSED 127.0.0.1:3333' }
Format Axios HTTP Error ("401 Unauthorized", "404 Not Found", "500 Internal Server Error" etc.)
Any status codes that falls outside the range of 2xx cause these errors.
const { axios } = require('@infotorg/winston-log-formats');
// Simulate creating an Axios Network error
const code = ''; // Make sense only for Network errors like ECONNREFUSED, ECONNRESET, ENOTFOUND, etc.
const message = 'Request failed with status code 404';
const config = {
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
headers: {
Accept: 'application/json, text/plain, */*',
'User-Agent': 'axios/1.3.3',
},
method: 'get',
url: 'https://example.com:1337/uploads/non-existent.svg',
requestStartedAt: 1675770391875,
data: undefined,
};
const request = {
method: 'GET',
path: '/uploads/non-existent.svg',
host: 'example.com',
protocol: 'https:',
};
const response = {
status: 404,
statusText: 'Not Found',
headers: {
'content-type': 'application/json; charset=utf-8',
'content-length': '94',
date: 'Sat Feb 18 2023 17:16:25 GMT',
connection: 'close',
},
config,
data: {
data: null,
error: { status: 404, name: 'NotFoundError', message: 'Not Found', details: {} },
},
responseTime: 100,
};
// This Axios HTTP Error that will be cought by axios interceptor and sent to logger
const axiosError = Object.assign(createAxiosError(message, config, code, request, response), { level: 'warn' });
// Transform Axios Network Error to log message
const error = axios().transform(
// Error instance (log entry)
axiosError,
// Options
{ meta: true }
);
console.log(error);
// { level: 'warn',
// message: 'GET https://example.com:1337/uploads/non-existent.svg 404 Not Found 100ms',
// meta:
// { res:
// { status: 404,
// statusText: 'Not Found',
// data:
// { data: null,
// error:
// { status: 404,
// name: 'NotFoundError',
// message: 'Not Found',
// details: {} } },
// responseTime: 100 },
// req:
// { timeout: '0',
// xsrfCookieName: 'XSRF-TOKEN',
// xsrfHeaderName: 'X-XSRF-TOKEN',
// method: 'get',
// url: 'https://example.com:1337/uploads/non-existent.svg',
// requestStartedAt: 1675770391875,
// data: undefined } },
// description: 'Axios error',
// [Symbol(level)]: 'warn',
// [Symbol(message)]: 'Request failed with status code 404' }
Description
The description
format adds the description
property to the info
object.
It accepts the following options:
- description: As a sting. If set then it applies to the log message. But if info object already has
description
property then it won't be overwritten.
// Description from opts object
const { description } = require('@infotorg/winston-log-formats');
const info = description().transform(
// Log entry
{
level: 'info',
message: 'my message',
},
// Options
{ description: 'API request' }
);
console.log(info);
// { level: 'info',
// message: 'my message',
// description: 'API request' }
// Description from info object
const { description } = require('@infotorg/winston-log-formats');
const info = description().transform(
// Log entry
{
level: 'info',
message: 'my message',
// Owerride description from opts object
description: 'Info description',
},
// Options
{ description: 'API request' }
);
console.log(info);
// { level: 'info',
// message: 'my message',
// description: 'Info description' }
Filter
The filter
format removes provided blackList
fields in the log message:
It accepts the following options:
- enabled: Enable/disable filter format output. Default value is
true
. - target: Target property for filtering in the info object. Default value is
meta
. - blackList: Fields that will be removed from the info object. It supports
dot notation
in the field names. Default value is[]
. Example:['req.data.password', 'req.headers.token', 'res.data.sensitive']
.
Dot notation is one way to access a property of an object. To use dot notation, write the name of the object, followed by a dot (.), followed by the name of the property.
Filter format usage
const { filter } = require('@infotorg/winston-log-formats');
const info = filter().transform(
// Log entry
{
level: 'info',
message: 'Test',
meta: {
req: {
url: 'https://example.com',
method: 'post',
headers: {
'X-Session': 'KdshmBaCdZdWxQ5yTpix.RCJmD7YONqSWyjhfzoP5',
'Content-type': 'application/json',
},
body: {
sensitive: 'sensitive data',
username: 'John',
},
requestStartedAt: 1675770391875,
},
},
},
// Manual options
{ enabled: true, target: 'meta', blackList: ['body.sensitive', 'headers', 'requestStartedAt'] }
);
console.log(info);
// { message: 'Test',
// meta:
// { req:
// { url: 'https://example.com',
// method: 'post',
// body: { username: 'John' } } } }
Mask
The mask
format masks sensitive data in the log message depends on provided options.
It accepts the following options:
- enabled: Enable/disable filter format output. Default value is
true
. - severity: Severity for masking data in a log. Default value is
partial
. Possible values areopen
,partial
, andstrict
. For more details, see mask data severity levels file. - target: property name in the log to mask. Default value is
meta
. It supportsdot notation
in the field names. Example:meta.req
. - whiteList: Fields that won't be masked. It supports
dot notation
in the field names. Default value is[]
that means all fields will be masked. Example:['req.data.username']
. - fullyMaskedFields: Fields that will be masked completely even if they are in the
whiteList
. It supportsdot notation
in the field names. Default value is[]
. Example:['req.data.password']
. - maskOptions: Masking options for the MaskData library. Default value is
{}
.
It should be applied as one of the last formats in the combine pipeline. After the
axios
andfilter
format but before finalizing formats likejson
anderrors
.
const { createLogger, format } = require('winston');
const { combine, errors, json, label, timestamp } = format;
const logger = createLogger({
level: loggerLevel,
format: combine(
label({
label: 'my-app',
}),
timestamp(),
// ...
requestId(),
axios({
// Options...
}),
filter({
// Options...
}),
// ----------------------
// Here is the proper place for the `mask` format
// ----------------------
mask({
// Options...
}),
errors({
// Options...
}),
json()
// Other finalizing formats...
),
transports: [],
// Other logger options...
});
Mask format usage
const { mask } = require('@infotorg/winston-log-formats');
const { MASK_DATA_SEVERITY_PARTIAL } = require('@infotorg/mask-data-severity-levels');
const info = mask().transform(
// Log entry
{
level: 'info',
message: 'Test',
meta: {
req: {
data: {
sensitive: 'your sensitive data',
password: 'super-secret-passwd',
},
headers: {
Accept: 'application/json, text/plain, */*',
'User-Agent': 'axios/1.3.3',
'X-SENSITIVE-HEADER': 'your sensitive header',
},
method: 'get',
requestStartedAt: 1675770391875,
timeout: 0,
url: 'https://example.com:1337/uploads/non-existent.svg',
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
},
res: {
data: {
data: { sensitive: 'Sensitive data - only for a client' },
error: { details: {}, message: 'Not Found', name: 'NotFoundError', status: 404 },
},
headers: {
'content-length': '94',
'content-type': 'application/json; charset=utf-8',
date: 'Sat Feb 18 2023 17:16:25 GMT',
},
responseTime: 100,
status: 404,
statusText: 'Not Found',
},
},
},
// Manual options
{
enabled: true,
severity: MASK_DATA_SEVERITY_PARTIAL,
target: 'meta',
// Options for mask data compatible with @coder.ua/mask-data package.
// Documentation: https://github.com/coderua/mask-data#default-options
maskOptions: {
// To limit the output String length to 30.
maxMaskedChars: 30,
// First 2 symbols that won't be masked
unmaskedStartChars: 2,
// Last 2 symbols that won't be masked
unmaskedEndChars: 2,
// Do not mask data with type 'boolean'
maskBoolean: false,
// Do not mask 'undefined' data
maskUndefined: false,
// Do not mask 'null' data
maskNull: false,
// Mask 'number' data
maskNumber: true,
},
whiteList: [
// Request related fields
'req.url',
'req.method',
// Response related fields
'res.status',
'res.method',
'res.headers.content-length',
'res.headers.content-type',
'res.statusText',
// Errors related fields
'res.data.error',
// ...
],
fullyMaskedFields: ['req.data.password'],
}
);
console.log(info);
// { message: 'Test',
// meta:
// { req:
// { data: { sensitive: 'yo***************ta', password: '****************' },
// headers:
// { Accept: 'application/json, text/plain, */*',
// 'User-Agent': 'ax********.0',
// 'X-SENSITIVE-HEADER': 'yo*****************er' },
// method: 'get',
// requestStartedAt: 16*********75,
// timeout: '*',
// url: 'https://example.com:1337/uploads/non-existent.svg',
// xsrfCookieName: 'XSRF-TOKEN',
// xsrfHeaderName: 'X-XSRF-TOKEN' },
// res:
// { data:
// { data: { sensitive: 'Se**************************nt' },
// error:
// { details: {},
// message: 'Not Found',
// name: 'NotFoundError',
// status: 404 } },
// headers:
// { 'content-length': '94',
// 'content-type': 'application/json; charset=utf-8',
// date: 'Sat Feb 18 2023 17:16:25 GMT' },
// responseTime: 100,
// status: 404,
// statusText: 'Not Found' } } }
Track ID
The trackId
format adds the trackId
field to each log message. It could be used to add some tracking information to each log message.
Can be used to add TrackId, RequestId to the log message.
It accepts the following options:
- enabled: Enable/disable
trackId
output. Default istrue
. - trackId: As a function that generates
trackId
or exact value. It bindsinfo
object as a last argument. Ifinfo
object already hastrackId
property then it won't be overwritten. - key: Field name/key to use for the trackId in the info object and log output. Default is
trackId
.
Log entry
info.trackId
has a higher priority than theopts.trackId
.
// Use trackId option (opts.trackId) as a function to generate trackId
const { trackId } = require('@infotorg/winston-log-formats');
const info = trackId().transform(
// Log entry
{
level: 'info',
message: 'my message',
},
// Options
{ enabled: true, trackId: (info) => '123456-test-track-id' }
);
console.log(info);
// { level: 'info',
// message: 'my message',
// trackId: '123456-test-track-id' }
// Use custom trackId key option (opts.key) => requestId
const { trackId } = require('@infotorg/winston-log-formats');
const info = trackId().transform(
// Log entry
{
level: 'info',
message: 'my message',
requestId: (info) => '123456-test-track-id',
},
// Options
{ key: 'requestId' }
);
console.log(info);
// { level: 'info',
// message: 'my message',
// requestId: '123456-test-track-id' }
// Use trackId option (opts.trackId) as a function with access to info object to generate trackId
const { trackId } = require('@infotorg/winston-log-formats');
const info = trackId().transform(
// Log entry
{
level: 'info',
message: 'my message',
node: 'node1',
},
// Options
{ trackId: (info) => `${info.node}:123456-test-track-id` }
);
console.log(info);
// { level: 'info',
// message: 'my message',
// trackId: 'node1:123456-test-track-id' }
// Use info.trackId value instead of trackId option (opts.trackId)
const { trackId } = require('@infotorg/winston-log-formats');
const info = trackId().transform(
// Log entry
{
level: 'info',
message: 'my message',
trackId: 'track-id-from-info',
},
// Options
{ trackId: (info) => '123456-test-track-id' }
);
console.log(info);
// { level: 'info',
// message: 'my message',
// trackId: 'track-id-from-info' }
// Use info.trackId as a function instead of trackId option (opts.trackId)
const { trackId } = require('@infotorg/winston-log-formats');
const info = trackId().transform(
// Log entry
{
level: 'info',
message: 'my message',
trackId: (info) => 'track-id-from-info-fn',
},
// Options
{ trackId: (info) => '123456-test-track-id' }
);
console.log(info);
// { level: 'info',
// message: 'my message',
// trackId: 'track-id-from-info-fn' }
Tests
Tests are written with jest
. They can be run with npm
:
npm run test