Skip to main content
BetterstackLogger is the Logger component’s Better Stack backend. It wraps the official @logtail/node client to ship structured logs to Better Stack’s Logs (Logtail) platform, attaching your data object to every entry and flattening exception details onto the payload. It implements the framework’s ILogger interface, so call sites stay provider-agnostic and you can swap the backend without touching the code that emits logs.

Why Better Stack

  • Hosted log management. Ship logs to Better Stack for search, dashboards, and alerting without running your own pipeline.
  • Structured payloads. Pass a data object and it’s sent alongside the message as structured fields.
  • Exception-aware. error() accepts an IException and flattens its name, status, and stack trace onto the payload.
  • Configurable ingest host. Point at a custom ingesting endpoint or use the Logtail default.
  • Buffered delivery. The client batches logs and flush() drains the buffer before the process exits.
  • Container-managed. Registered with @decorator.logger() and resolved from the container.

Installation

BetterstackLogger ships with @ooneex/logger and depends on the Logtail Node client.
bun add @ooneex/logger @logtail/node

Environment variables

VariableRequiredPurpose
BETTERSTACK_LOGGER_SOURCE_TOKENYesBetter Stack source token. Missing throws LoggerException (LOG_FAILED).
BETTERSTACK_LOGGER_INGESTING_HOSTNoCustom ingesting endpoint. Omitted uses the Logtail default.
BETTERSTACK_LOGGER_SOURCE_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxx
# Optional: point at your source's ingesting host
BETTERSTACK_LOGGER_INGESTING_HOST=https://s1234567.eu-nbg-2.betterstackdata.com
The source token is validated when BetterstackLogger is constructed, so a missing token fails fast at startup.

How it works

On construction, the backend creates a single Logtail client with your source token (and endpoint, if set). Each level method forwards the message and data to the matching Logtail method — error, warn, info, debug — while log and success map to info with a level field (LOG / SUCCESS) so the original level is preserved. When an IException is passed to error(), its name, status, and JSON stack trace are extracted and merged into the payload. Delivery is buffered; call flush() to drain it.
MethodPurpose
init()No-op; returns immediately.
error(message, data?)Ship an error. message may be a string or IException.
warn(message, data?)Ship a warning.
info(message, data?)Ship informational output.
debug(message, data?)Ship debug detail.
log(message, data?)Ship a general message (sent as info with level: "LOG").
success(message, data?)Ship a success message (sent as info with level: "SUCCESS").
flush()Drain the buffered logs.

Usage

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

const logger = container.get(BetterstackLogger);

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 });

// Flush before the process exits
await logger.flush();
error() accepts a plain message or an IException — when given an exception it flattens the name, status, and stack trace onto the payload:
import { container } from "@ooneex/container";
import { BetterstackLogger } from "@ooneex/logger";
import type { IException } from "@ooneex/exception";

const logger = container.get(BetterstackLogger);

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

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

  public completeOrder(userId: string, order: Order) {
    this.logger.success("Order completed", { userId, orderId: order.id });
  }
}

Use in the app

In an @ooneex/app application, register BetterstackLogger 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 @logtail/node
import { App } from "@ooneex/app";
import { BetterstackLogger } from "@ooneex/logger";

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

await app.run();
loggers accepts an array, so you can ship to Better Stack while also printing to the terminal in development:
import { App } from "@ooneex/app";
import { BetterstackLogger, TerminalLogger } from "@ooneex/logger";

const app = new App({
  routing: { prefix: "/api" },
  loggers: [TerminalLogger, BetterstackLogger],
});
Set BETTERSTACK_LOGGER_SOURCE_TOKEN (and optionally BETTERSTACK_LOGGER_INGESTING_HOST) in your .env.yml (or environment), then inject the logger into a service or controller:
import { inject } from "@ooneex/container";
import { BetterstackLogger } from "@ooneex/logger";

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

  public async completeOrder(userId: string, order: Order) {
    // ... persist the order ...
    this.logger.success("Order completed", { userId, orderId: order.id });
  }
}
Because delivery is buffered, flush the client before the process exits — for example from a shutdown hook:
import { container } from "@ooneex/container";
import { BetterstackLogger } from "@ooneex/logger";

process.on("SIGTERM", async () => {
  await container.get(BetterstackLogger).flush();
  process.exit(0);
});

Exceptions

BetterstackLogger throws LoggerException on misconfiguration, carrying a machine-readable key.
KeyWhen
LOG_FAILEDBetterstackLogger is constructed without BETTERSTACK_LOGGER_SOURCE_TOKEN.
import { LoggerException } from "@ooneex/logger";

try {
  const logger = container.get(BetterstackLogger);
} catch (error) {
  if (error instanceof LoggerException && error.key === "LOG_FAILED") {
    console.error("Better Stack is missing its source token", error.data);
  }
  throw error;
}

Best practices

  • Call flush() on exit. Delivery is buffered; drain it before the process terminates so you don’t drop the last logs.
  • Pass structured data, not interpolated strings. Keep the message stable and put variable detail in the data object so logs stay searchable.
  • Log exceptions as exceptions. Pass the IException to error() so the name, status, and stack trace are captured.
  • Keep the source token in the environment. Load it from .env; never hard-code it.
  • Match the ingest host to your source. Set BETTERSTACK_LOGGER_INGESTING_HOST when your source uses a region-specific endpoint.
For error and exception tracking specifically, see Better Stack Exceptions. See the Logger component for the provider interface and how logs flow through the framework.