Skip to content

mqnoy/lolog

Repository files navigation

🍭 lolog

Logging shouldn't be a bottleneck or a chore. lolog is a small, honest wrapper around Pino that handles the boring boilerplate so you don't have to — environment-specific output, secret redaction, and class-based context, all out of the box.

License: MIT TypeScript

Caution

v1.0.0 contains breaking changes. If you are upgrading from v0.0.7, please read the Migration Guide at the bottom of this page.


Why lolog?

Most teams end up writing the same setup code for Pino or Winston on every project: pretty-print in dev, JSON in prod, redact passwords, add a service name... It's always the same 50 lines. lolog does that for you:

  • Built for Speed — Uses Pino internally, the fastest Node.js logger.
  • Context-Aware — Decorators and child loggers tag your logs automatically.
  • Pretty in Dev, Clean in Prod — Switches output automatically based on NODE_ENV.
  • Secure by Defaultpassword, token, authorization, and friends are redacted automatically.
  • Easy to Configure — One setup() call at the top of your app is all it takes.

Installation

pnpm add @mqnoy/lolog
# or
npm install @mqnoy/lolog

Quick Start

The default export is a pre-configured logger instance. It just works.

import logger from '@mqnoy/lolog';

logger.info('Server started');
logger.warn({ diskSpace: '2%' }, 'Storage running low');

try {
  connectToDatabase();
} catch (err) {
  // Always put the Error object first — it ensures the stack trace is captured
  logger.error(err, 'Failed to connect to the database');
}

Global Configuration

Call logger.setup() once at the very top of your app entry point (e.g., index.ts). This configures the global logger instance that all decorated classes and child loggers inherit from.

import logger from '@mqnoy/lolog';

logger.setup({
  env: 'production',      // 'development' | 'production' — defaults to NODE_ENV
  serviceName: 'order-api',
  logLevel: 'info',       // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'
  transports: [
    { type: 'console', level: 'info' },
    {
      type: 'file',
      level: 'error',
      options: { destination: './logs/error.log' },
    },
  ],
});

Transport Options

Each transport in the transports array accepts the following shape:

{
  type: 'console' | 'file';
  level?: string;           // minimum level for this transport
  options?: {
    destination?: string;   // file path, for 'file' type only
    mkdir?: boolean;        // auto-create the directory, defaults to true
    colorize?: boolean;     // pino-pretty options, for 'console' type
    [key: string]: any;     // any other pino-pretty / pino/file option
  };
}

Note on level: Pino applies level filtering in two stages. The global logLevel acts as the first gate — logs below it are dropped immediately. Each transport's level then acts as a second filter. Always set the global logLevel to the lowest level any transport needs.


Usage with Decorators

The @Logger decorator automatically injects a contextual logger into your class. The context field in your logs will be set to the class name (or whatever string you pass in).

import { Logger, ILolog } from '@mqnoy/lolog';

@Logger('UserService')
class UserService {
  private readonly logger!: ILolog;

  async findById(id: string) {
    this.logger.debug({ userId: id }, 'Fetching user from database');
    // → log will include { context: 'UserService', userId: '...' }
  }
}

Important: Call logger.setup() before any classes using @Logger are initialized. Otherwise they'll use the default configuration.


Child Loggers

Child loggers are perfect for request-scoped tracing. Every log from a child inherits its parent's configuration and appends its own fields.

const requestLogger = logger.child({
  traceId: req.headers['x-trace-id'],
  method: req.method,
  path: req.path,
});

requestLogger.info('Request received');
requestLogger.debug({ userId: '42' }, 'Looking up user');
// Every log above will include the traceId, method, and path

Multiple Transports

You can fan out logs to multiple destinations simultaneously. Each transport has its own level filter, so you can, for example, send everything to the console but only errors to a file.

import { Lolog } from '@mqnoy/lolog';

const logger = new Lolog('MyApp', {
  env: 'development',
  logLevel: 'debug',
  transports: [
    { type: 'console', level: 'debug' },           // colorized dev output
    { type: 'file', level: 'error', options: { destination: './logs/error.log' } },
    { type: 'file', level: 'info',  options: { destination: './logs/combined.log' } },
  ],
});

logger.debug('This goes to console only');
logger.info('This goes to console and combined.log');
logger.error({ code: 'ERR_DB' }, 'This goes everywhere');

Note for short-lived scripts: Pino transports run in worker threads. If your process exits immediately after logging, add a short delay (e.g., setTimeout(() => {}, 1000)) to let the workers flush to disk.


Development vs Production

lolog switches its output format automatically based on NODE_ENV:

Environment Output Details
Development Pretty, colorized (pino-pretty) PID, hostname, and service metadata are hidden to keep the terminal clean
Production Raw NDJSON Structured JSON on stdout, ready for Datadog, CloudWatch, ELK, or Promtail

Automatic Redaction

lolog redacts the following fields by default. They'll appear as *** in your logs:

password, token, accessToken, refreshToken, secret, authorization, headers.authorization, req.headers.authorization

You can override this with a custom redact config:

logger.setup({
  redact: {
    paths: ['user.ssn', 'payment.cvv'],
    censor: '[REDACTED]',
  },
});

Migrating from v0.0.7 to v1.0.0

V1 is a ground-up rewrite. The class is still called Lolog, but almost everything under the hood changed. Here's what you need to update:

What Changed

Feature v0.0.7 (old) v1.0.0 (new)
Engine Winston Pino
Error argument order (message, error) (error, message)
Child logger method .setChild() .child()
Configuration Constructor only logger.setup() + constructor
Level labels Numeric in JSON String level_label field

1. Fix Error Logging

Pino serializes Error objects correctly only when they're the first argument.

// Before (v0.x)
logger.error('Database failed', err);

// After (v1.0)
logger.error(err, 'Database failed');

2. Fix Child Loggers

// Before (v0.x)
const child = logger.setChild({ requestId: '123' });

// After (v1.0)
const child = logger.child({ requestId: '123' });

3. Add Global Setup (Optional but Recommended)

Rather than passing config to every new Lolog(), configure the global instance once:

// index.ts — at the very top, before any other imports that use @Logger
import logger from '@mqnoy/lolog';

logger.setup({
  serviceName: 'my-service',
  logLevel: 'debug',
});

Built with care for the production Node.js ecosystem. Issues and contributions are welcome on GitHub.

About

Ease to logging your app with wrapping winston to own logger class which called `Lolog`

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors