Skip to main content
TerminalLogger is the Logger component’s console backend. It writes colorized, level-aware lines to stdout/stderr — with optional timestamps, symbols, structured data, and formatted stack traces — making it the natural choice for local development and CLI output. It implements the framework’s ILogger interface, so call sites stay provider-agnostic and you can swap to a remote or database backend without touching the code that emits logs.

Why Terminal Logger

  • Zero configuration. No tokens, hosts, or connection strings — it works the moment you resolve it.
  • Colorized levels. Each level (ERROR, WARN, INFO, DEBUG, LOG, SUCCESS) has its own color and symbol.
  • Structured data rendering. Pass a data object and scalar fields are printed as colorized key: value pairs.
  • Exception-aware. error() accepts an IException and renders its name, status, and a formatted stack trace.
  • Per-call display options. Toggle showArrow, showTimestamp, showLevel, and useSymbol on any call.
  • Container-managed. Registered with @decorator.logger() and resolved from the container.

Installation

TerminalLogger ships with @ooneex/logger.
bun add @ooneex/logger
It uses Bun.color for ANSI colorization, so it runs under the Bun runtime with no extra dependencies.

Environment variables

TerminalLogger needs no configuration — there are no environment variables to set.

How it works

Each level method formats a single line and writes it to the console. The line is assembled from an arrow, a timestamp, the level (as [LEVEL] or a symbol), and the colorized message. Any scalar fields in data are appended as colorized key: value pairs, and when an IException is passed to error(), its structured stack trace is rendered frame by frame. ERROR-level lines (and FATAL) are written to stderr; everything else goes to stdout.
MethodPurpose
init()No-op; returns immediately.
error(message, data?, options?)Log an error. message may be a string or IException.
warn(message, data?, options?)Log a warning.
info(message, data?, options?)Log informational output.
debug(message, data?, options?)Log debug detail.
log(message, data?, options?)Log a general message.
success(message, data?, options?)Log a success message.
The per-call display options (LoggerOptionsType):
OptionTypePurpose
showArrowbooleanShow the leading -> arrow. Defaults to true.
showTimestampbooleanShow the YYYY-MM-DD HH:mm:ss timestamp. Defaults to true.
showLevelbooleanShow the level label. Defaults to true.
useSymbolbooleanRender the level as a symbol instead of [LEVEL]. Defaults to false.

Usage

import { container } from "@ooneex/container";
import { TerminalLogger } from "@ooneex/logger";

const logger = container.get(TerminalLogger);

logger.info("User signed in", { userId: "123", email: "john@example.com" });
logger.success("Payment captured", { orderId: "ord_456", amount: 4999 });
logger.warn("Rate limit approaching", { remaining: 5 });

// Control terminal rendering per call
logger.debug("Cache miss", { key: "user:123" }, { useSymbol: true, showTimestamp: false });
error() accepts a plain message or an IException — when given an exception it renders the name, status, and a formatted stack trace:
import { container } from "@ooneex/container";
import { TerminalLogger } from "@ooneex/logger";
import type { IException } from "@ooneex/exception";

const logger = container.get(TerminalLogger);

try {
  await processPayment(order);
} catch (error) {
  logger.error(error as IException, { orderId: order.id });
}
Inject it into a service to log as part of your domain logic:
import { inject } from "@ooneex/container";
import { TerminalLogger } from "@ooneex/logger";

export class PaymentService {
  constructor(@inject(TerminalLogger) private readonly logger: TerminalLogger) {}

  public capture(order: Order) {
    this.logger.success("Payment captured", { orderId: order.id, amount: order.total });
  }
}

Use in the app

In an @ooneex/app application, register TerminalLogger by adding it to the loggers array of your App config. The loggers slot of AppConfigType is typed as LoggerClassType[], so you pass the class itself — the framework registers it with the container at startup.
bun add @ooneex/app @ooneex/logger
import { App } from "@ooneex/app";
import { TerminalLogger } from "@ooneex/logger";

const app = new App({
  routing: { prefix: "/api" },
  loggers: [TerminalLogger],
});

await app.run();
loggers accepts an array, so you can register more than one backend at once — for example terminal output alongside a persistent backend:
import { App } from "@ooneex/app";
import { DatabaseLogger, TerminalLogger } from "@ooneex/logger";

const app = new App({
  routing: { prefix: "/api" },
  loggers: [TerminalLogger, DatabaseLogger],
});
Once registered, inject it into a service or controller and log as part of your domain logic:
import { inject } from "@ooneex/container";
import { TerminalLogger } from "@ooneex/logger";

export class CheckoutService {
  constructor(@inject(TerminalLogger) private readonly logger: TerminalLogger) {}

  public async completeOrder(userId: string, order: Order) {
    // ... persist the order ...
    this.logger.success("Order completed", { userId, orderId: order.id });
  }
}
Because TerminalLogger needs no configuration, it’s ideal as the development default — swap to a remote or persistent backend (see Better Stack Logger, Database Logger) in production by changing which class you register.

Best practices

  • Use it for local and CLI output. It’s the zero-config development default; ship to a remote or database backend in production.
  • Pass structured data, not interpolated strings. Keep the message stable and put variable detail in the data object.
  • Log exceptions as exceptions. Pass the IException to error() so the name, status, and stack trace are rendered.
  • Tune the display per call. Use options to drop the timestamp or switch to symbols for compact CLI output.
  • Keep data scalar. Only scalar fields (string, number, boolean, bigint) are rendered; nested objects are skipped.
See the Logger component for the provider interface and how logs flow through the framework.