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.tsviacreateJsonStdoutLogger. - Backend routing is controlled by
parseObservabilityEnv, supportingnone,otel, andsentrymodes without code changes. - Application wrappers like
createAppLoggeranddenWebLoggerprovide sanitization and context injection. - Sentry integration is activated through
initSentryRuntime, which wraps the stdout sink withlogToSentry. - Graceful shutdown is handled by
shutdownRetainedTelemetryto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →