Skip to main content
BetterstackExceptionLogger is the Logger component’s Better Stack error-tracking backend. It wraps the official @sentry/node SDK to capture exceptions — with tags, contexts, breadcrumbs, and extra data — and ship them, along with structured logs, to Better Stack’s Sentry-compatible ingest. 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 errors.

Why Better Stack Exceptions

  • Rich exception capture. Errors are sent through Sentry with tags, contexts, and breadcrumbs for full debugging detail.
  • Exception-aware error(). Pass an IException and its name, status, date, and structured stack trace are attached automatically.
  • Structured logs too. warn, info, debug, log, and success ship as Sentry logs alongside your exceptions.
  • Sentry-compatible ingest. Reuses the Sentry SDK against Better Stack’s DSN, so existing Sentry tooling applies.
  • Buffered delivery. flush() drains pending events before the process exits.
  • Container-managed. Registered with @decorator.logger() and resolved from the container.

Installation

BetterstackExceptionLogger ships with @ooneex/logger and depends on the Sentry Node SDK.
bun add @ooneex/logger @sentry/node

Environment variables

VariableRequiredPurpose
BETTERSTACK_EXCEPTION_LOGGER_APPLICATION_TOKENYesBetter Stack application token used in the Sentry DSN. Missing throws LoggerException (TOKEN_REQUIRED).
BETTERSTACK_EXCEPTION_LOGGER_INGESTING_HOSTYesBetter Stack ingesting host used in the DSN. Missing throws LoggerException (EXCEPTION_LOG_FAILED).
BETTERSTACK_EXCEPTION_LOGGER_APPLICATION_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxx
BETTERSTACK_EXCEPTION_LOGGER_INGESTING_HOST=s1234567.eu-nbg-2.betterstackdata.com
Both values are validated when BetterstackExceptionLogger is constructed, so misconfiguration fails fast at startup.

How it works

On construction, the backend calls Sentry.init() with a DSN assembled from your application token and ingesting host, enabling logs and full trace sampling. error() opens a Sentry scope: a string message is captured as an Error with a breadcrumb (and your data as extras), while an IException is captured with exception.name/exception.status tags, an exception context (name, status, date, stack), a breadcrumb, and your data as extras. The remaining levels ship as Sentry logs — warn, info, and debug map directly, and log/success use info with a level field (LOG / SUCCESS). Delivery is buffered; flush() waits up to two seconds to drain it.
MethodPurpose
init()No-op; the SDK is initialized in the constructor.
error(message, data?)Capture an exception. message may be a string or IException.
warn(message, data?)Ship a warning log.
info(message, data?)Ship an informational log.
debug(message, data?)Ship a debug log.
log(message, data?)Ship a general log (sent as info with level: "LOG").
success(message, data?)Ship a success log (sent as info with level: "SUCCESS").
flush()Drain pending events (up to 2s).

Usage

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

const logger = container.get(BetterstackExceptionLogger);

logger.info("Worker started", { region: "eu" });
logger.warn("Retry scheduled", { attempt: 2 });

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

const logger = container.get(BetterstackExceptionLogger);

try {
  await processPayment(order);
} catch (error) {
  // Captured through Sentry with tags, context, and a breadcrumb
  logger.error(error as IException, { orderId: order.id });
}
Inject it into a service to capture errors as part of your domain logic:
import { inject } from "@ooneex/container";
import { BetterstackExceptionLogger } from "@ooneex/logger";

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

  public async charge(order: Order) {
    try {
      await this.gateway.charge(order);
    } catch (error) {
      this.logger.error(error as IException, { orderId: order.id });
      throw error;
    }
  }
}

Use in the app

In an @ooneex/app application, register BetterstackExceptionLogger on the onException slot of your App config. Unlike the general loggers array, onException of AppConfigType is typed as a single LoggerClassType and is the dedicated slot for the backend that handles uncaught exceptions — the framework registers it with the container and routes exceptions to it at startup.
bun add @ooneex/app @ooneex/logger @sentry/node
import { App } from "@ooneex/app";
import { BetterstackExceptionLogger, TerminalLogger } from "@ooneex/logger";

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

await app.run();
Here loggers handles your normal application logs while onException sends uncaught exceptions to Better Stack — the two slots are independent, so you can keep terminal output in development and still capture exceptions remotely. Set BETTERSTACK_EXCEPTION_LOGGER_APPLICATION_TOKEN and BETTERSTACK_EXCEPTION_LOGGER_INGESTING_HOST in your .env.yml (or environment) so the SDK can initialize. You can also inject the logger directly to capture handled errors inside your domain logic:
import { inject } from "@ooneex/container";
import { BetterstackExceptionLogger } from "@ooneex/logger";

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

  public async completeOrder(order: Order) {
    try {
      // ... persist the order ...
    } catch (error) {
      this.logger.error(error as IException, { orderId: order.id });
      throw error;
    }
  }
}
Because delivery is buffered, flush before the process exits — for example from a shutdown hook:
import { container } from "@ooneex/container";
import { BetterstackExceptionLogger } from "@ooneex/logger";

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

Exceptions

BetterstackExceptionLogger throws LoggerException on misconfiguration, carrying a machine-readable key.
KeyWhen
TOKEN_REQUIREDConstructed without BETTERSTACK_EXCEPTION_LOGGER_APPLICATION_TOKEN.
EXCEPTION_LOG_FAILEDConstructed without BETTERSTACK_EXCEPTION_LOGGER_INGESTING_HOST.
import { LoggerException } from "@ooneex/logger";

try {
  const logger = container.get(BetterstackExceptionLogger);
} catch (error) {
  if (error instanceof LoggerException && error.key === "TOKEN_REQUIRED") {
    console.error("Better Stack is missing its application 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 events.
  • Log exceptions as exceptions. Pass the IException to error() so its name, status, date, and stack trace are captured as Sentry context.
  • Attach context via data. Pass identifiers (order id, user id) as data so they land as Sentry extras for debugging.
  • Keep the token and host in the environment. Load both from .env; never hard-code them.
  • Use it for error tracking. For general structured log shipping, pair it with Better Stack Logger.
See the Logger component for the provider interface and how logs flow through the framework.