Skip to content

📝 tslog - Expressive TypeScript Logger for Node.js.

License

Notifications You must be signed in to change notification settings

jared-f/tslog

 
 

Repository files navigation

📝 tslog: Beautiful logging experience for Node.js with TypeScript support

lang: Typescript License: MIT npm version CI: Travis Coverage Status code style: prettier GitHub stars

Powerful, fast and expressive logging for Node.js

tslog pretty output

Highlights

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

Example

import { Logger } from "tslog";

const log: Logger = new Logger();
log.silly("I am a silly log.");

Install

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",
  },
}

Simple example

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."));

All Features

  • 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 for warn, 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: Catch console.log etc. that would otherwise be hard to find
  • Tested: 100% code coverage, CI

API documentation

Log object

TSDoc: interface: ILogObject

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:

Returned by each log method
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));
Printed out in JSON mode
new Logger({ type: "json" });

Resulting in the following output: tslog log level json

Forwarded to an attached transport

More details below

Log level

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 for trace by default. That means every trace 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: tslog log level structured

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

Child Logger

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",
});

Creating logger without source map support

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.

Settings

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.

type

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" });

Output: tslog log level json

Hint: Each JSON log is printed in one line, making it easily parsable by external services.

instanceName

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
name

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 });
minLevel

default: "silly"

Minimum log level to be captured by this logger. Possible values are: silly, trace, debug, info, warn, error, fatal

requestId

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
exposeStack

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 });

tslog with a stack trace

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.

exposeErrorCodeFrame

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.

tslog with a code frame

ignoreStackLevels

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).

suppressStdOutput

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 });
overwriteConsole

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.

colorizePrettyLogs

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.

logLevelsColors

This setting allows you to overwrite the default log level colors of tslog.

Possible styles are:

prettyInspectHighlightStyles

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

delimiter

default: [ ] (space) Set a custom pretty log delimiter.

dateTimePattern

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.

dateTimeTimezone

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)

prefix

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.
A requestId 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
maskValuesOfKeys

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: '[***]',
//   }
// }
maskAnyRegEx

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).

maskPlaceholder

default: "[***]"

String to use for masking of secrets (s. maskAnyRegEx & maskValuesOfKeys)

printLogMessageInNewLine

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.

displayDateTime

default: true

Defines whether the date time should be displayed.

displayLogLevel

default: true

Defines whether the log level should be displayed.

displayInstanceName

default: false

Defines whether the instance name (e.g. host name) should be displayed.

displayLoggerName

default: true

Defines whether the optional logger name should be displayed.

displayRequestId

default: true

Defines whether the requestId should be displayed, if set and available (s. requestId).

displayFunctionName

default: true

Defines whether the class and method or function name should be displayed.

displayTypes

default: false

Defines whether type information (typeof) of every attribute passed to tslog should be displayed.

displayFilePath

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 containing node_modules.
stdOut and stdErr

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).

Transports

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.

Simple transport example

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"
);
Storing logs in a file

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."]}

Helper

prettyError

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 object
  • print - 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)
printPrettyLog

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);

About

📝 tslog - Expressive TypeScript Logger for Node.js.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 98.4%
  • Shell 1.1%
  • JavaScript 0.5%