Skip to main content

Logging Guidelines

How logging is done in this project.

Overview

This project uses a unified logging system based on Pino with OpenTelemetry integration.

The logging infrastructure is located in packages/core/src/telemetry/:

  • logger.ts - Logger interface and Pino implementation
  • global-logger.ts - Global logger singleton with lazy binding

Key Principles

  1. Use the telemetry logger for all debug/service logs - Never use console.log for internal debugging
  2. Use structured logging - Pass context as object, message as string
  3. Use module-level child loggers - Create a child logger at module scope for consistent context

Two Types of Console Output

TypeWhen to UseImplementation
CLI User OutputDirect user-facing messages in CLI commandspackages/core/src/cli/lib/output.ts functions (output(), outputTable(), outputSuccess())
Debug/Service LogsInternal state, errors, debuggingTelemetry logger (log.info(), log.error(), etc.)

Log Levels

LevelUsageExample
traceVery detailed diagnostic informationInternal state dumps, loop iterations
debugDiagnostic information useful for debuggingFunction entry/exit, intermediate values
infoGeneral operational informationServer started, request completed, connection established
warnPotentially problematic situationsDeprecated API usage, retry attempts, missing optional config
errorError events that might still allow operationFailed requests, caught exceptions, validation failures
fatalSevere errors causing process terminationUnrecoverable errors, startup failures

Mapping from console

BeforeAfter
console.log()log.info() or log.debug()
console.warn()log.warn()
console.error()log.error()

Structured Logging

Module-Level Child Logger Pattern

Every module should create a child logger at the top level:

import { logger as globalLogger } from "../telemetry";

// Create module-level child logger
const log = globalLogger.child({ module: "my-module-name" });

// Use throughout the module
log.info({ userId: "123" }, "Processing request");
log.error({ err: error, context: { userId: "123" } }, "Failed to process");

Structured Log Format

Always pass context as the first parameter (object), message as the second (string):

// Correct - structured logging
log.info({ userId, action: "login" }, "User authenticated");
log.error({ err: error, requestId }, "Request failed");

// Incorrect - avoid string interpolation
log.info(`User ${userId} authenticated`); // Don't do this

Error Logging Pattern

Always use err as the key for error objects (Pino convention):

try {
await doSomething();
} catch (error) {
log.error({ err: error, context: { userId } }, "Operation failed");
}

What to Log

Always Log

  • Service startup/shutdown
  • Connection established/lost (database, WebSocket, external services)
  • Authentication events (success/failure)
  • Significant state changes
  • Request completion with timing
  • Error conditions with context

Example Patterns

// Service lifecycle
log.info({ port: 3000 }, "Server started");
log.info("Server stopping...");

// Connections
log.info({ channelName: "telegram" }, "Connected to channel");
log.warn({ channelName: "discord", retryIn: 5000 }, "Connection lost, reconnecting...");

// Operations
log.info({ taskId, duration: 1234 }, "Task completed");
log.debug({ messageId, from: senderId }, "Message received");

// Errors
log.error({ err: error, taskId }, "Task execution failed");

What NOT to Log

Never Log

  • Passwords and authentication tokens (API keys, JWT tokens, session secrets)
  • Personal Identifiable Information (PII) in production (email addresses, phone numbers, full names)
  • Credit card or financial data
  • Full request/response bodies containing sensitive data
  • Environment variables that may contain secrets

Automatic PII Redaction

The telemetry logger automatically redacts sensitive fields:

  • password, secret, token, apiKey, api_key
  • authorization, cookie
  • Fields matching patterns like *token, *secret, *key

Safe Logging Examples

// Safe - log ID references, not sensitive data
log.info({ userId: user.id }, "User authenticated");

// Unsafe - don't log tokens
log.debug({ token: authToken }); // Don't do this!

// Safe alternative
log.debug({ tokenPrefix: authToken.slice(0, 8) + "..." }, "Token issued");

Best Practices

1. Consistent Module Naming

Use kebab-case for module names matching the file name:

// In telegram-poller.ts
const log = globalLogger.child({ module: "telegram-poller" });

// In preview.ts
const log = globalLogger.child({ module: "preview" });

2. Avoid Over-Logging

  • Use debug level for verbose output that's only useful during development
  • Use info level sparingly - only for significant events
  • Never log in tight loops without throttling

3. Include Actionable Context

Log enough context to debug issues without looking at source code:

// Good - includes context for debugging
log.error({
err: error,
taskId,
attempt: retryCount,
maxAttempts: 3
}, "Task execution failed");

// Poor - missing context
log.error("Task failed");

4. Use Appropriate Levels for Different Environments

  • Development: debug level
  • Production: info level (or warn for high-volume services)

The log level is configured via the LOG_LEVEL environment variable.

Log Storage

~/.viben/telemetry/logs/
└── YYYY-MM-DD.jsonl # Aggregated by date

Migration Checklist

When migrating a file from console.log to the telemetry logger:

  1. Add import: import { logger as globalLogger } from "../telemetry";
  2. Create child logger: const log = globalLogger.child({ module: "module-name" });
  3. Replace each console call:
    • console.log("message") -> log.info("message")
    • console.log(`value: ${x}`) -> log.info({ x }, "value")
    • console.error("error:", err) -> log.error({ err }, "error")
  4. Verify with pnpm typecheck and pnpm build

Forbidden Patterns

Using console.log

// Wrong
console.log('User created:', user);

// Correct
logger.info({ userId: user.id }, 'User created');