Overview

Structured wide-event logging for Vercube, powered by evlog

The Logger module is Vercube's logging system. As of v1 it is a thin, dependency-injected wrapper around evlog - a structured, wide-event logger with pretty development output, JSON production output, PII redaction, sampling and pluggable drains/adapters.

You keep the familiar Logger DI token and the debug / info / warn / error methods; underneath, every call becomes an evlog event.

Installation

$ pnpm add @vercube/logger

@vercube/core already depends on @vercube/logger and binds it automatically - you only install it directly when using it standalone.

Quick Start

The logger is bound for you

createApp binds the evlog-backed Logger and configures it from your app config. You normally don't bind anything yourself:

vercube.config.ts
import { defineConfig } from '@vercube/core';

export default defineConfig({
  // forwarded to evlog's `minLevel`
  logLevel: 'debug',
});

If you bootstrap a container manually, bind it explicitly:

src/container.ts
import { Container } from '@vercube/di';
import { Logger, BaseLogger } from '@vercube/logger';

export function setupContainer(container: Container): void {
  container.bind(Logger, BaseLogger);
  container.get(Logger).configure({
    logLevel: 'info',
    pretty: process.env.NODE_ENV !== 'production',
  });
}

Inject Logger into Services

src/services/UserService.ts
import { Inject } from '@vercube/di';
import { Logger } from '@vercube/logger';

export class UserService {
  @Inject(Logger)
  private logger!: Logger;

  async createUser(data: CreateUserDto) {
    this.logger.debug('UserService::createUser', 'creating user');

    try {
      const user = await this.database.createUser(data);
      this.logger.info('UserService::createUser', { userId: user.id });
      return user;
    } catch (error) {
      this.logger.error('UserService::createUser', error as Error);
      throw error;
    }
  }
}

Use Logger in Controllers

src/controllers/UserController.ts
import { Controller, Get, Post } from '@vercube/core';
import { Inject } from '@vercube/di';
import { Logger } from '@vercube/logger';

@Controller('/users')
export class UserController {
  @Inject(Logger)
  private logger!: Logger;

  @Post('/')
  async create(req: Request) {
    this.logger.info('POST /users', 'creating new user');
    // ...
    return Response.json({ ok: true });
  }
}

Core Concepts

Logger

Logger is the DI token injected throughout your application. It exposes the four classic level methods plus a small wide-event API:

  • debug() / info() / warn() / error() - fire-and-forget logs, one evlog event per call.
  • set(context) - merge structured fields into every subsequent event.
  • getContext() - read the accumulated context.
  • child(context) - derive a logger that inherits context plus extra fields.
  • emit(overrides?) - flush the accumulated context as a single wide event, then reset.

How arguments map to events

The variadic arguments are translated into an evlog event:

CallResulting event
info('tag', 'message')tagged log tagmessage
warn('something happened'){ message: 'something happened' }
error(err){ error: { name, message, stack } }
error('tag', err){ message: 'tag', error: {...} }
info('tag', { userId: 1 }){ message: 'tag', userId: 1 }

Any object argument is merged into the event; any Error is captured under error.

Log Levels

Levels are hierarchical and identical to evlog's:

debug → info → warn → error

logLevel maps to evlog's minLevel (a hard threshold for the simple log API). Order: debug < info < warn < error.

Wide events

A "wide event" is a single, richly-structured log line that accumulates context over an operation, then is emitted once. Use set() to add context and emit() to flush it:

logger.set({ jobId: 'sync-001', queue: 'emails' });
logger.set({ processed: 120 });
logger.emit({ outcome: 'success' });
// → one event: { jobId, queue, processed, outcome }

For per-request wide events (method, path, status, duration), Vercube ships the EvlogMiddleware in @vercube/core, enabled by default. Disable it with requestLogging: false in your app config.

Configuration

configure() accepts evlog's full LoggerConfig plus the logLevel alias:

container.get(Logger).configure({
  logLevel: 'info',          // alias for evlog `minLevel`
  pretty: true,              // human-readable output (auto: true in dev)
  silent: false,             // suppress console output (drains still run)
  stringify: true,           // emit JSON strings when pretty is off
  redact: true,              // PII redaction (auto: true in production)
  env: { service: 'api' },   // environment context
  sampling: { rates: { info: 0.5 } },
  drain: (ctx) => sendToBackend(ctx.event),
});

Sending logs elsewhere (drains & adapters)

evlog's drains and ready-made adapters (Axiom, OTLP, Sentry, Datadog, …) are available directly from the evlog package and re-exported from @vercube/logger. See the Drains & Adapters page.

Previous

Drains & Adapters

Control output format and ship logs to external backends with evlog

Next