Skip to main content

Logging & Tracing

Logging

For convenience, DBOS provides a logging facility accessed via DBOS.logger. For example:

DBOS.logger.info("Welcome to DBOS!");

Entries logged to the DBOS logger are automatically augmented with DBOS context information, such as the current workflow state.

You can configure the log level of this built-in logger:

DBOS.setConfig({
name: 'my-app',
logLevel: "info",
});
await DBOS.launch();

Setting logLevel also affects any log messages emitted by the DBOS library.

Custom Logger

By default, the DBOS logger writes to the console (or exports its logs over OTLP when enableOTLP is set). To route all of DBOS's internal logging to your own logging system instead, supply a custom logger that implements the DLogger interface:

import { DLogger, ContextualMetadata, StackTrace } from '@dbos-inc/dbos-sdk';

class MyCustomLogger implements DLogger {
info(logEntry: unknown, metadata?: ContextualMetadata): void {}
debug(logEntry: unknown, metadata?: ContextualMetadata): void {}
warn(logEntry: unknown, metadata?: ContextualMetadata): void {}
error(inputError: unknown, metadata?: ContextualMetadata & StackTrace): void {}
}

Pass your implementation through the logger field in DBOS configuration:

DBOS.setConfig({
name: 'my-app',
logger: myCustomLogger,
});
await DBOS.launch();

You can also supply a custom logger to the DBOS Client:

const client = await DBOSClient.create({
systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL,
logger: myCustomLogger,
});

When a custom logger is set, DBOS directs all its internal logging to it (including DBOS.logger calls in your workflows and steps), replacing the built-in console and OTLP log sinks. Keep the following contract in mind when implementing DLogger:

  • Log entries arrive as strings. DBOS stringifies non-string entries before delegating. error() receives the message of an Error, with its stack trace in metadata.stack.
  • Context metadata is provided via the span. When called from a workflow or step, metadata.span?.attributes carries the operation context (workflow ID, operation name and type, etc.).
  • Level routing is your responsibility. DBOS does not filter by logLevel before delegating; your implementation decides what to do with each level.
  • OTLP log export is disabled. Logs are not sent over OTLP even if enableOTLP is on (tracing is unaffected).
  • The logger's lifecycle is yours. DBOS never flushes or closes it.
  • Do not log back through DBOS.logger from within your implementation, as this could cause infinite recursion.

Tracing

DBOS automatically constructs OpenTelemetry spans for every workflow and step. Spans are hierarchical: a step's span is a child of its workflow's span. If the workflow was started from an already-traced operation, such as an instrumented HTTP request, the workflow span is a child of that operation's span and shares its trace. Otherwise, DBOS starts a new trace.

DBOS emits spans on the global OpenTelemetry tracer. This means that if your application already sends telemetry to an observability provider through OpenTelemetry, DBOS spans automatically join your existing traces. You don't need to set up a separate export pipeline just for DBOS.

info

OpenTelemetry support in DBOS is optional. To use it, install the DBOS OpenTelemetry dependencies:

npm i @dbos-inc/otel@latest

Connecting DBOS to your observability provider

This is the recommended way to use tracing with DBOS. If you already send telemetry to an observability provider (Datadog, Langfuse, Honeycomb, Grafana, Logfire, Jaeger, ...) through OpenTelemetry, you can have DBOS workflow and step spans join your existing traces with no extra setup.

There are two steps:

  1. Register your provider's OpenTelemetry TracerProvider before calling DBOS.launch().
  2. Set tracingEnabled: true in your DBOS configuration.

Set up your provider, then configure and launch DBOS, using whichever option matches your platform:

Most observability platforms—Honeycomb, Grafana, Logfire, a self-hosted OpenTelemetry Collector, or local Jaeger—accept the OpenTelemetry Protocol (OTLP). Point an OTLP exporter at your endpoint:

import { DBOS } from '@dbos-inc/dbos-sdk';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';

// Set up your provider
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
// headers: { 'x-api-key': process.env.OTEL_API_KEY }, // if your provider needs auth
}),
});
sdk.start();

// Configure and launch DBOS
DBOS.setConfig({
name: 'my-app',
tracingEnabled: true, // DBOS creates spans; your provider exports them
otelAttributeFormat: 'semconv', // emit dbos.* attribute names (recommended)
});
await DBOS.launch();
tip

Set up your provider before calling DBOS.launch(): DBOS adopts whichever global provider exists at launch (importing DBOS or registering workflows beforehand is fine).

Adding custom attributes and events

Within a workflow or step, you can access the current span and enrich it. This is useful for attaching domain data such as a user ID, a request size, or LLM token usage. Access it via DBOS.span, or in the standard OpenTelemetry way with trace.getSpan(context.active()):

const llmWorkflow = DBOS.registerWorkflow(async (prompt: string) => {
return await DBOS.runStep(async () => {
const span = DBOS.span; // equivalently: trace.getSpan(context.active())
span?.setAttribute('gen_ai.request.model', 'claude-opus-4-8');

const response = await callModel(prompt);

span?.setAttribute('gen_ai.usage.input_tokens', response.usage.inputTokens);
span?.setAttribute('gen_ai.usage.output_tokens', response.usage.outputTokens);
span?.addEvent('model call complete');
return response.text;
}, { name: 'callModel' });
}, { name: 'llmWorkflow' });

Your provider exports these attributes on the span alongside DBOS's own, so they appear together in your dashboards.

Letting DBOS export traces directly

If you don't already run an observability provider, DBOS can export traces and logs itself to any OpenTelemetry Protocol (OTLP)-compliant receiver. Set enableOTLP: true and configure your export endpoints:

DBOS.setConfig({
name: 'my-app',
enableOTLP: true,
otlpTracesEndpoints: ["http://localhost:4318/v1/traces"],
otlpLogsEndpoints: ["http://localhost:4318/v1/logs"],
});
await DBOS.launch();

If otlpLogsEndpoints is configured, enableOTLP also exports your DBOS.logger logs over OTLP. For example, try using Jaeger to visualize the traces of your local application, or export your logs and traces to Logfire.

Metrics

Using Conductor, you can also scrape metrics about your applications' workflows, steps, and executors from a Prometheus-compatible endpoint. See Metrics for details.