OpenTelemetry and observability

Beignet separates local inspection from production telemetry:

The OpenTelemetry provider is optional. Without it, Beignet keeps request IDs and W3C trace correlation for logs and devtools without adding an OpenTelemetry runtime dependency.

Install

bun add @beignet/provider-tracing-opentelemetry @opentelemetry/api

Install and register the SDK appropriate for the deployment. For a Next.js app on Vercel:

bun add @vercel/otel
import { registerOTel } from "@vercel/otel";

const state = globalThis as typeof globalThis & {
  __appTelemetryRegistered?: boolean;
};

export function registerTelemetry() {
  if (state.__appTelemetryRegistered) return;
  registerOTel({ serviceName: "my-app" });
  state.__appTelemetryRegistered = true;
}
import { registerTelemetry } from "@/lib/telemetry";

export function register() {
  registerTelemetry();
}

The root instrumentation.ts bootstraps the Next.js process. Standalone job workers, task and schedule commands, outbox drains, and scripts must call the same idempotent registerTelemetry() function before they initialize their Beignet server. Installing the Beignet provider without registering an SDK is safe, but OpenTelemetry's global tracer and meter remain no-ops.

Beignet does not start an SDK, choose an exporter, or create a process shutdown hook.

Register the provider

Place the OpenTelemetry provider after devtools and before providers whose external work should contribute operation metrics and span events:

import { createDevtoolsProvider } from "@beignet/devtools";
import { createOpenTelemetryTracingProvider } from "@beignet/provider-tracing-opentelemetry";
import { createResendMailProvider } from "@beignet/provider-mail-resend";
import { registerTelemetry } from "@/lib/telemetry";

registerTelemetry();

export const providers = [
  createDevtoolsProvider(),
  createOpenTelemetryTracingProvider(),
  createResendMailProvider(),
] as const;

The provider contributes ports.tracing and replaces ports.instrumentation with a composed sink. OpenTelemetry receives later provider events while the earlier devtools sink continues to receive its enabled watchers.

Devtools is optional. In production, the provider can be installed without a devtools provider.

Active spans

When ports.tracing is installed, Beignet wraps these execution boundaries:

BoundarySpan nameKind
HTTP requestbeignet.request <contract>server or internal
Use casebeignet.use_case <name>internal
Listenerbeignet.listener <name>consumer
Job handlerbeignet.job <name>consumer
Schedule handlerbeignet.schedule <name>consumer
Task handlerbeignet.task <name>internal

The current span remains active across asynchronous handler work. Provider SDKs that use the OpenTelemetry API can therefore create children of the correct request, use case, listener, or workflow span without importing a Beignet-specific global.

Incoming traceparent and tracestate headers continue an HTTP trace. If a host adapter already established an active OpenTelemetry request span, Beignet's request span becomes its child instead of creating a competing root.

Metrics

The provider records:

Durations use milliseconds. Attributes are limited to stable operation names, types, outcomes, attempts, and provider names. Beignet does not attach request bodies, payloads, tenant IDs, user IDs, message IDs, error messages, or stacks to these metrics.

Custom TraceOperation values may attach rich span-only attributes. Add only bounded dimensions to metricAttributes; never copy request, actor, tenant, or payload values into metric labels.

An SDK that only configures tracing may leave the global OpenTelemetry meter as a no-op. Register a meter provider to export metrics, inject a configured meter, or set meter: false when the app intentionally exports traces only.

Errors and Sentry

Failed spans set error status and error.type. Exception messages and stacks remain out of spans by default. Enable recordExceptions only after reviewing the exporter and backend redaction policy:

createOpenTelemetryTracingProvider({
  recordExceptions: true,
});

Keep exception capture in ErrorReporterPort. When Sentry and another OpenTelemetry SDK are both installed, prevent Sentry from registering a second pipeline:

createSentryErrorReportingProvider({
  init: { skipOpenTelemetrySetup: true },
});

Beignet does not export OpenTelemetry logs. Continue using LoggerPort for structured runtime logs and AuditLogPort for durable business activity.

Propagation boundary

This release propagates context through HTTP and nested in-process execution. It does not add trace fields to durable outbox rows, Redis event envelopes, or provider job payloads. Work handled by another process starts a new Beignet trace unless that provider's own instrumentation establishes an active context.

That boundary is explicit because durable envelope changes need a compatibility and retention policy. Use stable job, event, message, and request IDs to correlate those traces in the meantime.

Serverless behavior

The Beignet provider performs no network setup and starts no workers, timers, or background loops. It creates spans and records metrics through the SDK that the app already registered. The host integration or app-owned SDK remains responsible for batching, exporting, and flushing telemetry within the platform's lifecycle. Register the SDK once in every process type; Next.js does not invoke its root instrumentation.ts for standalone workers or CLI commands.

See Request lifecycle for HTTP correlation, Devtools for the local event timeline, Logging for diagnostic logs, and Error reporting for exception capture.