Powerful, fast and expressive logging for Node.js
โก Batteries included, native V8 integration
๐ฎโ๏ธ Fully typed with TypeScript support (exact code position)
๐ Pretty or JSON
output
โญ๏ธ Supports circular structures
๐ฆธ Custom pluggable loggers
๐
Object and error interpolation
๐ต๏ธโ Code surrounding error position (code frame)
๐ค Stack trace through native V8 API
๐ Works for TypeScript and JavaScript
๐จโ๐งโ๐ฆ Child logger with inheritance
๐ Mask/hide secrets and keys
๐ Native support for request IDs (async_hooks
, AsyncLocalStorage
)
๐ฆ CommonJS and ES Modules with tree shaking support
๐งฒ Optionally catch all console
logs
โ๏ธ well documented
import { Logger } from "tslog";
const log: Logger = new Logger();
log.silly("I am a silly log.");
npm install tslog
Enable TypeScript source map support:
This feature enables tslog
to reference a correct line number in your TypeScript source code.
// tsconfig.json
{
// ...
"compilerOptions": {
// ...
"sourceMap": true,
// we recommend using a current ES version
"target": "es2019",
}
}
import { Logger } from "tslog";
const log: Logger = new Logger({ name: "myLogger" });
log.silly("I am a silly log.");
log.trace("I am a trace log with a stack trace.");
log.debug("I am a debug log.");
log.info("I am an info log.");
log.warn("I am a warn log with a json object:", {foo: "bar"});
log.error("I am an error log.");
log.fatal(new Error("I am a pretty Error with a stacktrace."));
- Log level:
silly
,trace
,debug
,info
,warn
,error
,fatal
(different colors) - Output to std: Structured/pretty output (easy parsable
tab
delimiters),JSON
or suppressed - Attachable transports: Send logs to an external log aggregation services, file system, database, or email/slack/sms/you name it...
- StdOut or StdErr depends on log level: stdout for
silly
,trace
,debug
,info
and stderr forwarn
,error
,fatal
- Minimum log level per output:
minLevel
level can be set individually per transport - Fully typed: Written in TypeScript, fully typed, API checked with api-extractor, TSDoc documented
- Source maps lookup: Shows exact position also in TypeScript code (compile-to-JS), one click to IDE position
- Stack trace: Callsites through native V8 stack trace API, excludes internal entries
- CommonJS and ES Modules
- Tree shake support via ESM import syntax (tree-shaking)
- Pretty Error: Errors and stack traces printed in a structured way and fully accessible through JSON (e.g. external Log services)
- Code frame:
tslog
captures and displays the source code that lead to an error, making it easier to debug - Object/JSON highlighting: Nicely prints out an object using native Node.js
utils.inspect
method - Instance Name: Logs capture instance name (default host name) making it easy to distinguish logs coming from different instances (e.g. serverless)
- Named Logger: Logger can be named (e.g. useful for packages/modules and monorepos)
- Highly configurable: All settings can be changed through a typed object, also during run time (e.g. log level)
- Adjust settings at runtime Change settings at runtime with immediate impact (e.g. log level)
- Child Logger with inheritance Powerful child loggers with settings inheritance, also at runtime
- RequestId: Group logs originated from a request and follow them all the way down the promise chain
- Secrets masking: Prevent passwords and secrets from sneaking into log files by masking them
- Short paths: Paths are relative to the root of the application folder
- Prefixes: Prefix log messages and bequeath prefixes to child loggers
- Types: Display type information
- Runtime-agnostic: Works with
ts-node
,ts-node-dev
, as well as compiled down to JavaScript - Optionally overwrite
console
: Catchconsole.log
etc. that would otherwise be hard to find - Tested: 100% code coverage, CI
Internally tslog
creates an object representing every available information around a particular log message, including errors, stack trace etc.
This information can become quite handy in case you want to work with this data or forward it to an external log service.
interface ILogObject {
/** Optional name of the instance this application is running on. */
instanceName?: string;
/** Optional name of the logger or empty string. */
loggerName?: string;
/* Name of the host */
hostname: string;
/** Optional unique request ID */
requestId?: string;
/** Timestamp */
date: Date;
/** Log level name (e.g. debug) */
logLevel: silly | trace | debug | info | warn | error | fatal;
/** Log level ID (e.g. 3) */
logLevelId: 0 | 1 | 2 | 3 | 4 | 5 | 6;
/** Log arguments */
argumentsArray: (unknown | {
/** Is this object an error? */
isError: true;
/** Name of the error*/
name: string;
/** Error message */
message: string;
/** additional Error details */
details: object;
/** native Error object */
nativeError: Error;
/** Stack trace of the error */
stack: IStackFrame[];
/** Code frame of the error */
codeFrame?: {
firstLineNumber: number;
lineNumber: number;
columnNumber: number | null;
linesBefore: string[];
relevantLine: string;
linesAfter: string[];
};
})[];
/** Optional Log stack trace */
stack?: {
/** Relative path based on the main folder */
filePath: string;
/** Full path */
fullFilePath: string;
/** Name of the file */
fileName: string;
/** Line number */
lineNumber: number | null;
/** Column Name */
columnNumber: number | null;
/** Called from constructor */
isConstructor: boolean | null;
/** Name of the function */
functionName: string | null;
/** Name of the class */
typeName: string | null;
/** Name of the Method */
methodName: string | null;
}[];
}
There are three ways to access this object:
import { Logger, ILogObject } from "tslog";
const log: Logger = new Logger();
const logWithTrace: ILogObject = log.trace("I am a trace log with a stack trace.");
console.log(JSON.stringify(logWithTrace, null, 2));
new Logger({ type: "json" });
Resulting in the following output:
tslog
is highly customizable, however, it follows convention over configuration when it comes to log levels.
Internally a log level is represented by a numeric ID.
Available log levels are:
0: silly
, 1: trace
, 2: debug
, 3: info
, 4: warn
, 5: error
, 6: fatal
Per default log level 0 - 3 are written to stdout
and 4 - 6 are written to stderr
.
Each log level is printed in a different color, that is customizable through the settings object.
Hint: Log level
trace
behaves a bit differently compared to all the other log levels. While it is possible to activate a stack trace for every log level, it is already activated fortrace
by default. That means everytrace
log will also automatically capture and print its entire stack trace.
import { Logger } from "tslog";
const log: Logger = new Logger();
log.silly("I am a silly log.");
log.trace("I am a trace log with a stack trace.");
log.debug("I am a debug log.");
log.info("I am an info log.");
log.warn("I am a warn log with a json object:", {foo: "bar"});
log.error("I am an error log.");
log.fatal(new Error("I am a pretty Error with a stacktrace."));
Structured (aka. pretty) log level output would look like this:
Hint: Each logging method has a return type, which is a JSON representation of the log message (
ILogObject
). You can use this object to access its stack trace etc. More details
Each tslog
Logger instance can create child loggers and bequeath its settings to a child.
It is also possible to overwrite every setting when creating a child.
Child loggers are a powerful feature when building a modular application and due to its inheritance make it easy to configure the entire application.
Use getChildLogger()
to create a child logger based on the current instance.
Example:
const logger: Logger = new Logger({ name: "MainLogger" });
const childLogger: Logger = logger.getChildLogger({ name: "FirstChild" });
const grandchildLogger: Logger = childLogger.getChildLogger({ name: "GrandChild" });
By default, Logger
creates instance with stack trace's source mapping support. For some cases, it may not be needed. LoggerWithoutCallSite
returns same interface as Logger
does and only disabling call site wrapping for source map.
import { Logger, LoggerWithoutCallSite } from 'tslog';
const logger = new Logger(...);
const loggerWithoutCallSite = new LoggerWithoutCallSite(...);
Since tslog
supports tree-shaking via esm import syntax, importing LoggerWithoutCallSite
without Logger
will reduce overall bundle size.
As tslog
follows convention over configuration, it already comes with reasonable default settings.
Therefor all settings are optional. Nevertheless, they can be flexibly adapted to your own needs.
All possible settings are defined in the ISettingsParam
interface and modern IDEs will provide auto-completion accordingly.
You can use setSettings()
to adjust settings at runtime.
Hint: When changing settings at runtime this alternation would also propagate to every child loggers, as long as it has not been overwritten down the hierarchy.
default: "pretty"
Possible values: "json" | "pretty" | "hidden"
You can either pretty
print logs, print them as json
or hide them all together with hidden
(e.g. when using custom transports).
Having json
as an output format is particularly useful, if you want to forward your logs directly from your std
to another log service.
Instead of parsing a pretty output, most log services prefer a JSON representation.
Hint: Printing in
json
gives you direct access to all the available information, like stack trace and code frame and so on.
new Logger({ type: "json" });
Hint: Each JSON log is printed in one line, making it easily parsable by external services.
default: os.hostname
(hidden by default)
You can provide each logger with the name of the instance, making it easy to distinguish logs from different machines. This approach works well in the serverless environment as well, allowing you to filter all logs coming from a certain instance.
Per default instanceName
is pre-filled with the hostname
of your environment, which can be overwritten.
However, this value is hidden by default in order to keep the log clean and tidy.
You can change this behavior by setting displayInstanceName
to true
.
const logger: Logger = new Logger({ displayInstanceName: true });
// Would print out the host name of your machine
const logger: Logger = new Logger({ displayInstanceName: true, instanceName: "ABC" });
// Would print out ABC as the name of this instance
default: undefined
Each logger has an optional name, that is hidden by default. You can change this behavior by setting displayLoggerName
to true
.
This setting is particularly interesting when working in a monorepo
,
giving you the possibility to provide each module/package with its own logger and being able to distinguish logs coming from different parts of your application.
new Logger({ name: "myLogger" });
Additional Setting:
setCallerAsLoggerName: false
When setting to true
tslog
will use caller name as the default name of the logger.
new Logger({ setCallerAsLoggerName: true });
default: "silly"
Minimum log level to be captured by this logger.
Possible values are: silly
, trace
, debug
, info
, warn
, error
, fatal
default: undefined
โ Keep track of all subsequent calls and promises originated from a single request (e.g. HTTP).
In a real world application a call to an API would lead to many logs produced across the entire application.
When debugging it can get quite handy to be able to group all logs based by a unique identifier requestId
.
A requestId
can either be a string
or a function.
A string is suitable when you create a child logger for each request, while a function is helpful, when you need to reuse the same logger and need to obtain a requistId
dynamically.
With Node.js 13.10, we got a new feature called AsyncLocalStorage.
It has also been backported to Node.js v12.17.0 and of course it works with Node.js >= 14.
However it is still marked as experimental.
Here is a blog post by Andrey Pechkurov describing AsyncLocalStorage
and performing a small performance comparison.
Hint: If you prefer to use a more proven (yet slower) approach, you may want to check out
cls-hooked
.
Even though tslog
is generic enough and works with any of these solutions our example is based on AsyncLocalStorage
.
tslog
also works with any API framework (like Express
, Koa
, Hapi
and so on), but we are going to use Koa
in this example.
Based on this example it should be rather easy to create an Express
or another middleware.
Some provides (e.g. Heroku
) already set a X-Request-ID
header, which we are going to use or fallback to a short ID generated by nanoid
.
In this example every subsequent logger is a child logger of the main logger and thus inherits all of its settings making requestId
available throughout the entire application without any further ado.
index.ts:
import * as Koa from 'koa';
import { AsyncLocalStorage } from "async_hooks";
import { customAlphabet } from "nanoid";
const asyncLocalStorage: AsyncLocalStorage<{ "requestId": string }> = new AsyncLocalStorage();
const logger: Logger = new Logger({
name: "Server",
requestId: (): string => {
return asyncLocalStorage.getStore()?.requestId as string;
}
});
export { logger };
const app: Koa = new Koa();
/** START AsyncLocalStorage requestId middleware **/
koaApp.use(async (ctx: Koa.Context, next: Koa.Next) => {
// use x-request-id or fallback to a nanoid
const requestId: string = ctx.request.headers['x-request-id'] || customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 6)();
// every other Koa middleware will run within the AsyncLocalStorage context
await asyncLocalStorage.run({ requestId }, async () => {
return next();
});
});
/** END AsyncLocalStorage requestId middleware **/
other_file.ts:
import { logger } from "./index";
const childLogger = logger.getChildLogger({ name: "ChildLogger" });
childLogger.info("Log containing requestId"); // <-- will contain a requestId
default: false
Usually, only Errors and log level trace
logs would capture the entire stack trace.
By enabling this option every stack trace of every log message is going to be captured.
new Logger({ exposeStack: true });
Hint: When working in an IDE like WebStorm or an editor like VSCode you can click on the path leading you directly to the position in your source code.
default: true
A nice feature of tslog
is to capture the code frame around the error caught, showing the exact location of the error.
While it comes quite handy during development, it also means that the source file (*.js or *.ts) needs to be loaded.
When running in production, you probably want as much performance as possible and since errors are analyzed at a later point in time,
you may want to disable this feature.
In order to keep the output clean and tidy, code frame does not follow into node_modules
.
new Logger({ exposeErrorCodeFrame: false });
Hint: By default 5 lines before and after the line with the error will be displayed. You can adjust this setting with
exposeErrorCodeFrameLinesBeforeAndAfter
.
default: 3
Defines how many stack levels should be ignored.
tslog
adds additional 3 layers to the stack and that the reason why the default is set to 3
.
You can increase this number, if you want to add additional layers (e.g. a factory class or a facade).
default: false
It is possible to connect multiple transports (external loggers) to tslog
(see below).
In this case it might be useful to suppress all output.
new Logger({ suppressStdOutput: true });
default: false
tslog
is designed to be used directly through its API.
However, there might be use cases, where you want to make sure to capture all logs,
even though they might occur in a library or somebody else's code.
Or maybe you prefer or used to work with console
, like console.log
, console.warn
and so on.
In this case, you can advise tslog
to overwrite the default behavior of console
.
Hint: It is only possible to overwrite
console
once, so the last attempt wins. If you wish to do so, I would recommend to have a designated logger for this purpose.
new Logger({ name: "console", overwriteConsole: true });
tslog
applies the following mapping:
console.log
:silly
console.trace
:trace
console.info
:info
console.warn
:warn
console.error
:error
There is no console.fatal
.
default: true
By default pretty
output is colorized with ANSI escape codes. If you prefer a plain output, you can disable the colorization with this setting.
This setting allows you to overwrite the default log level colors of tslog
.
Possible styles are:
This setting allows you to overwrite the default colors of tslog
used for the native Node.js utils.inspect
interpolation.
More Details: Customizing util.inspect colors
default: [ ] (space)
Set a custom pretty log delimiter.
default: "year-month-day hour:minute:second.millisecond"
Caution! Changing this pattern will affect performance (invocation of Intl.DateTimeFormat)
Change the way tslog
prints out the date.
Based on Intl.DateTimeFormat formatToParts with additional milliseconds, you can use type as a placeholder.
Available placeholders are: day
, dayPeriod
, era
, hour
, literal
, minute
, month
, second
, millisecond
, timeZoneName
, weekday
and year
.
default: "utc"
Define in which timezone the date should be printed in.
Possible values are utc
and IANA (Internet Assigned Numbers Authority) based timezones, e.g. Europe/Berlin
, Europe/Moscow
and so on.
Hint: If you want to use your local time zone, you can set:
dateTimeTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone
Caution! Changing this pattern will affect performance (invocation of Intl.DateTimeFormat)
default: []
Prefix every log message with an array of additional attributes.
Prefixes propagate to child loggers and can help to follow a chain of promises.
In addition to requestId
, prefixes can help further distinguish different parts of a request.
Hint: A good example could be a GraphQL request, that by design could consist of multiple queries and/or mutations.
ArequestId
would mark all the operations and prefixes can help to distinguish separate queries/mutations inside of this request.
Example:
const logger: Logger = new Logger({
name: "MainLogger",
prefix: ["main", "parent"],
});
logger.info("MainLogger message");
// Output:
// INFO [MainLogger] main parent MainLogger message
const childLogger: Logger = logger.getChildLogger({
name: "FirstChild",
prefix: ["child1"],
});
childLogger.info("child1 message");
// Output:
// INFO [FirstChild] main parent child1 child1 message
const grandchildLogger: Logger = childLogger.getChildLogger({
name: "GrandChild",
prefix: ["grandchild1"],
});
grandchildLogger.silly("grandchild1 message");
// Output:
// INFO [GrandChild] main parent child1 grandchild1 grandchild1 message
// change settings during runtime
childLogger.setSettings({ prefix: ["renamedChild1"] });
grandchildLogger.silly("grandchild1 second message")
// Output:
// INFO [GrandChild] main parent renamedChild1 grandchild1 second message
default: ["password"]
One of the most common ways of a password/secrets breach is through log files.
Given the central position of tslog
as the collecting hub of all application logs, it's only natural to use it as a filter.
maskValuesOfKeys
makes it possible to hide/mask all values of fields from objects passed into tslog
.
maskValuesOfKeys
is case insensitive!
const secretiveLogger = new Logger({
name: "SecretiveLogger",
maskValuesOfKeys: ["test", "authorization", "password"],
});
let secretiveObject = {
Authorization: 1234567,
regularString: "I am just a regular string.",
user: {
name: "Test",
otherString: "Test123.567",
password: "swordfish",
}
};
secretiveLogger.info(secretiveObject);
// Output:
// INFO [SecretiveLogger]
// {
// Authorization: '[***]',
// regularString: 'I am just a regular string.',
// user: {
// name: "Test",
// otherString: "Test123.567",
// password: '[***]',
// }
// }
default: []
When maskValuesOfKeys
is just not enough, and you really want to make sure no secrets get populated, you can also use maskAnyRegEx
to mask every occurrence of a string matching a particular RegEx.
Hint: It will also mask keys if it encounters a matching pattern.
maskValuesOfKeys
is case sensitive!
const verySecretiveLogger = new Logger({
name: "SecretiveLogger",
maskValuesOfKeys: ["test", "authorization", "password"],
maskAnyRegEx: ["pass.*"], // mask every string that starts with "pass"
});
let secretiveObject = {
Authorization: 1234567,
regularString: "I am just a regular string.",
user: {
name: "Test",
otherString: "pass1234.567",
password: "swordfish",
}
};
verySecretiveLogger.info(secretiveObject);
// Output:
// INFO [SecretiveLogger]
// {
// Authorization: '[***]',
// regularString: 'I am just a regular string.',
// user: {
// name: "Test",
// otherString: "[***].567",
// password: '[***]',
// }
// }
Hint: useful for API keys and other secrets (e.g. from ENVs).
default: "[***]"
String to use for masking of secrets (s. maskAnyRegEx
& maskValuesOfKeys
)
default: false
By default tslog
uses tab
delimiters for separation of the meta information (date, log level, etc.) and the log parameters.
Since the meta information can become quite long, you may want to prefer to print the log attributes in a new line.
default: true
Defines whether the date time should be displayed.
default: true
Defines whether the log level should be displayed.
default: false
Defines whether the instance name (e.g. host name) should be displayed.
default: true
Defines whether the optional logger name should be displayed.
default: true
Defines whether the requestId
should be displayed, if set and available (s. requestId
).
default: true
Defines whether the class and method or function name should be displayed.
default: false
Defines whether type information (typeof
) of every attribute passed to tslog
should be displayed.
default: hideNodeModulesOnly
Defines whether file path and line should be displayed or not. There are 3 possible settgins:
hidden
displayAll
hideNodeModulesOnly
(default): This setting will hide all file paths containingnode_modules
.
This both settings allow you to replace the default stdOut
and stdErr
WriteStreams.
However, this would lead to a colorized output. We use this setting mostly for testing purposes.
If you want to redirect the output or directly access any logged object, we advise you to attach a transport (see below).
tslog
focuses on the one thing it does well: capturing logs.
Therefore, there is no build-in file system logging, log rotation, or similar.
Per default all logs go to stdOut
and stdErr
respectively.
However, you can easily attach as many transports as you wish, enabling you to do fancy stuff like sending a message to Slack or Telegram in case of an urgent error.
When attaching a transport, you must implement every log level. All of them could be potentially handled by the same function, though.
Each transport can have its own minLevel
.
Attached transports are also inherited to child loggers.
Here is a very simple implementation used in our jest tests:
import { ILogObject, Logger } from "tslog";
const transportLogs: ILogObject[] = [];
function logToTransport(logObject: ILogObject) {
transportLogs.push(logObject);
}
const logger: Logger = new Logger();
logger.attachTransport(
{
silly: logToTransport,
debug: logToTransport,
trace: logToTransport,
info: logToTransport,
warn: logToTransport,
error: logToTransport,
fatal: logToTransport,
},
"debug"
);
Here is an example how to store all logs in a file.
import { ILogObject, Logger } from "tslog";
import { appendFileSync } from "fs";
function logToTransport(logObject: ILogObject) {
appendFileSync("logs.txt", JSON.stringify(logObject) + "\n");
}
const logger: Logger = new Logger();
logger.attachTransport(
{
silly: logToTransport,
debug: logToTransport,
trace: logToTransport,
info: logToTransport,
warn: logToTransport,
error: logToTransport,
fatal: logToTransport,
},
"debug"
);
logger.debug("I am a debug log.");
logger.info("I am an info log.");
logger.warn("I am a warn log with a json object:", { foo: "bar" });
Result: logs.txt
{"loggerName":"","date":"2020-04-27T15:24:04.334Z","logLevel":"debug","logLevelId":2,"filePath":"example/index.ts","fullFilePath":"/Users/eugene/Development/workspace/tslog/example/index.ts","fileName":"index.ts","lineNumber":56,"columnNumber":5,"isConstructor":false,"functionName":null,"typeName":"Object","methodName":null,"argumentsArray":["I am a debug log."]}
{"loggerName":"","date":"2020-04-27T15:24:04.334Z","logLevel":"info","logLevelId":3,"filePath":"example/index.ts","fullFilePath":"/Users/eugene/Development/workspace/tslog/example/index.ts","fileName":"index.ts","lineNumber":57,"columnNumber":5,"isConstructor":false,"functionName":null,"typeName":"Object","methodName":null,"argumentsArray":["I am an info log."]}
Sometimes you just want to pretty print an error without having to log it, or maybe just catch its call sites, or it's stack frame? If so, this helper is for you.
prettyError
exposes all the awesomeness of tslog
without the actual logging. A possible use case could be in a CLI, or other internal helper tools.
Example:
const logger: Logger = new Logger();
const err: Error = new Error("Test Error");
logger.prettyError(err);
Additional Parameters:
error
- Error objectprint
- Print the error or return only? (default: true)exposeErrorCodeFrame
- Should the code frame be exposed? (default: true)exposeStackTrace
- Should the stack trace be exposed? (default: true)stackOffset
- Offset lines of the stack trace (default: 0)stackLimit
- Limit number of lines of the stack trace (default: Infinity)std
- Which std should the output be printed to? (default: stdErr)
If you just want to pretty print an error on a custom output (for adding a new transport for example),
you can just call logger.printPrettyLog(myStd, myLogObject)
where myStd is an instance of IStd
(e.g. process.stdout
, process.stderr
or even a custom one, see example below):
class SimpleStd implements IStd {
constructor(private _buffer: string = '') {}
write(message: string) {
this._buffer += message;
}
get buffer(): string {
return this._buffer;
}
}
const logger: Logger = new Logger();
const myStd = new SimpleStd();
const myLogObject = logger.info("Hello World");
logger.printPrettyLog(myStd, myLogObject);