How is logging handled in OpenWork: Structured JSON and Sentry Integration

OpenWork handles logging through a structured JSON stdout logger with optional Sentry or OpenTelemetry backends, configured via environment variables and implemented across services using factory functions like createJsonStdoutLogger and createAppLogger.

The OpenWork repository (different-ai/openwork) implements a centralized observability layer that separates log formatting from backend routing. At startup, services parse environment configuration to determine whether logs flow to stdout only, Sentry, or OpenTelemetry, ensuring consistent structured output while maintaining deployment flexibility.

Core Logging Architecture

Environment-Based Configuration

Logging behavior is determined at runtime by the parseObservabilityEnv function, which reads environment variables to select the backend type—none, otel, or sentry—and extracts service metadata. This configuration parser is located in the utilities package and invoked from service-specific entry points such as ee/apps/den-api/src/observability/config.ts [source].

Structured JSON Logger Factory

The foundation of the logging stack is createJsonStdoutLogger, defined in ee/packages/utils/src/observability.ts [source]. This factory returns a logger instance that emits single-line JSON objects containing standard fields (timestamp, level, service, message) alongside arbitrary contextual data. The structured format enables automated parsing by log aggregation platforms without regex-based extraction.

Application-Level Logger Implementation

API Gateway Logger

Services wrap the base JSON logger with domain-specific sanitization and context enrichment. The Den API gateway uses createAppLogger from ee/apps/den-api/src/observability/logger.ts [source] to instantiate loggers that automatically inject service names and trace contexts. This wrapper sanitizes sensitive fields before emission while preserving the underlying JSON schema required by downstream processors.

Den-Web Runtime Logger

The Den-Web UI maintains a global logger singleton via denWebLogger in ee/apps/den-web/observability/runtime-logger.ts [source]. This module manages the active sink implementation and exposes shutdownRetainedTelemetry, which flushes buffered logs and traces during graceful shutdown on SIGTERM or SIGINT signals.

Backend Integrations and Switching

Sentry Integration

When DEN_OBSERVABILITY_BACKEND is set to sentry, the system invokes initSentryRuntime from ee/apps/den-web/observability/sentry-runtime.ts [source]. This function wraps the default stdout sink with logToSentry, forwarding all log calls to the Sentry SDK while maintaining local JSON output. The integration captures error contexts, distributed traces, and log levels within the Sentry dashboard.

Configuration via Environment Variables

Switching backends requires zero code changes—only environment variable updates. Setting DEN_OBSERVABILITY_BACKEND=sentry alongside SENTRY_DSN and optional SENTRY_TRACES_SAMPLE_RATE triggers the Sentry runtime initialization. The configuration validates these variables during startup, ensuring the appropriate backend is instantiated before application code executes.

Practical Usage Examples

Creating a Logger for a Custom Component

import { createAppLogger } from "@openwork-ee/apps/den-api/src/observability/logger";

const logger = createAppLogger({ fields: { component: "my_custom_worker" } });

logger.info("worker started", { pid: process.pid });
logger.error("unexpected error", { error: err });

This factory uses the same JSON stdout logger defined in observability.ts and automatically inherits the service name from the parsed configuration.

Switching the Backend to Sentry


# .env or exported shell variables

DEN_OBSERVABILITY_BACKEND=sentry
SENTRY_DSN=https://public_key@host/12345
SENTRY_TRACES_SAMPLE_RATE=0.05

Upon restart, parseObservabilityEnv creates a SentryBackendObservabilityConfig, and initSentryRuntime replaces the sink. Every subsequent logger.info() call routes to both Sentry and JSON stdout.

Adding Contextual Fields with Child Loggers

const requestLogger = logger.child({ requestId: req.id });
requestLogger.debug("received payload", { payloadSize: req.body.length });

The child method, implemented in createJsonStdoutLogger, performs shallow merges of parent and child fields.

Emitting Logs from the Den-Web UI

import { denWebLogger } from "./observability/runtime-logger";

denWebLogger.warn("session timeout", { userId: session.userId });

denWebLogger respects the current sink state—whether JSON stdout or Sentry—without requiring caller modifications.

Summary

  • Structured JSON stdout is the default logging format, implemented in observability.ts via createJsonStdoutLogger.
  • Backend routing is controlled by parseObservabilityEnv, supporting none, otel, and sentry modes without code changes.
  • Application wrappers like createAppLogger and denWebLogger provide sanitization and context injection.
  • Sentry integration is activated through initSentryRuntime, which wraps the stdout sink with logToSentry.
  • Graceful shutdown is handled by shutdownRetainedTelemetry to ensure buffered logs are flushed before process exit.

Frequently Asked Questions

How do I switch from stdout logging to Sentry in OpenWork?

Set the environment variable DEN_OBSERVABILITY_BACKEND=sentry and provide a valid SENTRY_DSN. The initSentryRuntime function automatically intercepts log calls and forwards them to Sentry while preserving local JSON output. No application code modifications are required.

Where is the structured JSON logger defined in the OpenWork codebase?

The core implementation resides in ee/packages/utils/src/observability.ts [source]. This file exports createJsonStdoutLogger, which constructs the JSON-emitting logger used across all services.

How does OpenWork handle log sanitization and context?

Services use wrapper factories such as createAppLogger (Den API) and denWebLogger (Den-Web) to sanitize sensitive fields and inject runtime context like service names and trace IDs. The child method allows request-scoped field injection without mutating the parent logger instance.

What happens to logs during graceful shutdown?

The runtime-logger.ts module exposes shutdownRetainedTelemetry, which flushes any buffered logs and traces to the active backend (Sentry or stdout) when the process receives SIGTERM or SIGINT. This prevents log loss during container orchestration scaling events or deployments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →