Skip to content

Logging

Nimbus provides a structured logger that outputs consistent, formatted log messages to the console. The logger integrates with Deno's native OpenTelemetry support for automatic log export to observability backends.

An in Depth Example

This guide also has an in depth example of a working application built with Nimbus. Combining DDD, CQRS and Event Sourcing.

Check out the In Depth Example page to learn how everything is connected and works out in a real-world application.

Setup and Configuration

Configure the logger at application startup using setupLogger():

typescript
import {
    createLogTruncator,
    jsonLogFormatter,
    parseLogLevel,
    prettyLogFormatter,
    setupLogger,
} from "@nimbus-cqrs/core";
import process from "node:process";

setupLogger({
    logLevel: parseLogLevel(process.env.LOG_LEVEL),
    formatter:
        process.env.LOG_FORMAT === "pretty"
            ? prettyLogFormatter
            : jsonLogFormatter,
    useConsoleColors: process.env.LOG_FORMAT === "pretty",
    truncator: createLogTruncator(),
});

Configuration Options

OptionTypeDefaultDescription
logLevelLogLevel'silent'Minimum level to output
formatterLogFormatterjsonLogFormatterFunction to format log records
useConsoleColorsbooleanfalseEnable colored output (for pretty formatter)
truncatorLogTruncatorundefinedOptional truncator for log inputs

Log Levels

Nimbus supports the following log levels in order of severity:

LevelMethodDescription
debugconsole.debug()Detailed debugging information
infoconsole.info()General information about application flow
warnconsole.warn()Warning conditions that should be reviewed
errorconsole.error()Error conditions that need attention
criticalconsole.error()Critical failures requiring immediate action
silent(none)Disables all log output

Messages below the configured log level are silently ignored.

Parsing Log Levels

Use parseLogLevel() to safely parse environment variables:

typescript
import { parseLogLevel } from "@nimbus-cqrs/core";

// Returns 'info' if LOG_LEVEL is 'info', otherwise returns default 'silent'
const level = parseLogLevel(process.env.LOG_LEVEL);

Basic Usage

Access the logger using getLogger():

typescript
import { getLogger } from "@nimbus-cqrs/core";

const logger = getLogger();

logger.debug({
    message: "Processing request",
    category: "API",
    data: { method: "POST", path: "/users" },
    correlationId: "550e8400-e29b-41d4-a716-446655440000",
});

logger.info({
    message: "User created successfully",
    category: "Users",
    data: { userId: "12345" },
});

logger.warn({
    message: "Rate limit approaching",
    category: "API",
    data: { currentRate: 95, maxRate: 100 },
});

logger.error({
    message: "Failed to process payment",
    category: "Payments",
    error: new Error("Payment gateway timeout"),
    correlationId: "550e8400-e29b-41d4-a716-446655440000",
});

logger.critical({
    message: "Database connection lost",
    category: "Database",
    error: new Error("Connection refused"),
});

Log Input

The log input object can contain the following properties:

PropertyTypeDescription
messagestringRequired. The log message
categorystringOptional category for grouping logs (defaults to 'Default')
dataRecord<string, unknown>Optional structured data to include
errorErrorOptional error with stack trace
correlationIdstringOptional ID for tracing related operations
skipTruncationbooleanWhen true, skips the configured truncator for this call (not emitted in output)

Formatters

Nimbus provides two built-in formatters:

JSON Formatter (Production)

Outputs structured JSON for easy parsing by log aggregation tools:

typescript
import { jsonLogFormatter, setupLogger } from "@nimbus-cqrs/core";

setupLogger({
    logLevel: "info",
    formatter: jsonLogFormatter,
});

// Output:
// {"timestamp":"2025-01-22T10:00:00.000Z","level":"info","category":"Users","message":"User created","data":{"userId":"123"}}

Pretty Formatter (Development)

Outputs human-readable colored logs for development:

typescript
import { prettyLogFormatter, setupLogger, getLogger } from "@nimbus-cqrs/core";

setupLogger({
    logLevel: "debug",
    formatter: prettyLogFormatter,
    useConsoleColors: true,
});

getLogger().debug({
    message: "My message",
    category: "Category",
    data: { userId: "12345" },
});

// Outputs:
// [Category] DEBUG :: My message
// {
//   userId: '12345'
// }

Truncation

By default, log inputs are passed through unchanged. To avoid flooding log pipelines with large payloads, configure a truncator via setupLogger.

Use the built-in factory, or supply a custom LogTruncator:

typescript
import { createLogTruncator, setupLogger } from "@nimbus-cqrs/core";

// Built-in defaults
setupLogger({
    truncator: createLogTruncator(),
});

// Override limits
setupLogger({
    truncator: createLogTruncator({
        maxBytes: 8_192,
        maxArrayItems: 50,
        maxObjectKeys: 50,
        maxDepth: 8,
        maxCategoryLength: 50,
        maxMessageLength: 100,
        maxStackLength: 200,
        maxDataStringLength: 200,
    }),
});

The built-in truncator processes each LogInput field separately and leaves correlationId untouched:

FieldBehavior
message / categoryString length caps
dataStructural limits (strings, arrays, object keys, depth, circular refs, common JS types), then a byte-size cliff for oversized or unserializable data
errorStays Error-shaped; truncates message / stack; copies enumerable custom fields; walks cause and aggregate errors (including non-Error values) up to maxDepth
correlationIdNever truncated
skipTruncationLogger control flag; when true, truncation is skipped for that call

Inside data, common values are normalized before limits apply: bigint → string, Date → ISO string, RegExp → pattern string, Map / Set → arrays (with maxArrayItems), Error → plain object, and ArrayBuffer / typed arrays → size summaries.

Truncator options

OptionDefaultApplies to
maxBytes16384data size cliff only
maxArrayItems50Arrays inside data (including converted Map / Set)
maxObjectKeys50Own enumerable keys on plain objects inside data
maxDepth8Object depth in data; also bounds error.cause chains
maxCategoryLength50category
maxMessageLength200message and error.message
maxStackLength200error.stack
maxDataStringLength200Strings inside data

If a configured truncator throws, the logger fails open: it warns to the console and logs the original input.

Pass skipTruncation: true on a single log call to bypass truncation when you intentionally need the full payload:

typescript
getLogger().info({
    message: "Full debug dump",
    data: hugePayload,
    skipTruncation: true,
});

OpenTelemetry Integration

When combined with Deno's native OpenTelemetry support, logs are automatically exported alongside traces and metrics. See the Observability documentation for details on enabling OTEL export.

bash
export OTEL_DENO=true
export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-otlp-endpoint.com/otlp"
export OTEL_SERVICE_NAME=your-service-name

deno run src/main.ts

Default Settings

If setupLogger() is not called, the logger uses these defaults:

typescript
const defaultSettings = {
    logLevel: "silent",
    formatter: jsonLogFormatter,
    useConsoleColors: false,
    // truncator: undefined (no truncation)
};

This means logs are silent by default - you must explicitly configure the logger to see output.

Nimbus Internal Logs

All Nimbus components (Router, EventBus, etc.) use the same logger configured via setupLogger(). This ensures consistent log formatting and level filtering across your application.

Best Practices

Use Categories

Group related logs with consistent category names:

typescript
logger.info({ message: "Query executed", category: "Database" });
logger.info({ message: "Request received", category: "API" });
logger.info({ message: "Email sent", category: "Notifications" });

Include Correlation IDs

Always include correlation IDs when available for distributed tracing:

typescript
logger.info({
    message: "Processing order",
    category: "Orders",
    data: { orderId: order.id },
    correlationId: command.correlationid,
});

Log Errors Properly

Use the dedicated error property for errors to preserve stack traces:

typescript
// ✅ Good - Error is properly captured
logger.error({
    message: "Failed to save user",
    error: error,
    correlationId: command.correlationid,
});

// ❌ Bad - Stack trace is lost
logger.error({
    message: "Failed to save user",
    data: { error: error.message },
});

Use Appropriate Log Levels

  • debug: Detailed info for debugging (disabled in production)
  • info: Normal application flow
  • warn: Unexpected but recoverable situations
  • error: Errors that need investigation
  • critical: Failures requiring immediate action