diff --git a/src/backend/index.ts b/src/backend/index.ts
index 9d5f66519..b2a8d55cf 100644
--- a/src/backend/index.ts
+++ b/src/backend/index.ts
@@ -19,6 +19,7 @@
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
+import { isSpanContextValid, trace } from '@opentelemetry/api';
import { puterClients } from './clients';
import { puterControllers } from './controllers';
import { puterDrivers } from './drivers';
@@ -26,6 +27,7 @@ import { PuterServer } from './server';
import { puterServices } from './services';
import { puterStores } from './stores';
import type { IConfig } from './types';
+import { installJsonConsole } from './util/jsonConsole.js';
// Config resolution order:
// 1. `process.env.PUTER_CONFIG_PATH` — absolute path to a config file. Used
@@ -167,6 +169,23 @@ const loadConfig = (): IConfig => {
// if called directly, start the server
if (require.main === module) {
const config = loadConfig();
+
+ // Structured logging: when `log_format: "json"`, replace the global console
+ // so each call emits one JSON line (level, timestamp, msg, and the active
+ // trace id) — one event per call, so a line-oriented log collector can't
+ // split stack traces across events. Installed here rather than in the OTel
+ // preload so it applies even when telemetry is disabled; the trace id is
+ // simply absent when no span is active.
+ if (config.log_format === 'json') {
+ installJsonConsole({
+ getTraceContext: () => {
+ const ctx = trace.getActiveSpan()?.spanContext();
+ if (!ctx || !isSpanContextValid(ctx)) return undefined;
+ return { traceId: ctx.traceId, spanId: ctx.spanId };
+ },
+ });
+ }
+
const server = new PuterServer(
config,
puterClients,
diff --git a/src/backend/telemetry.ts b/src/backend/telemetry.ts
index 3b15c8ce1..d8aed4257 100644
--- a/src/backend/telemetry.ts
+++ b/src/backend/telemetry.ts
@@ -17,7 +17,6 @@
* along with this program. If not, see .
*/
-import { isSpanContextValid, trace } from '@opentelemetry/api';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
@@ -33,59 +32,6 @@ import {
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
-import { existsSync, readFileSync } from 'node:fs';
-import path from 'node:path';
-import { installJsonConsole } from './util/jsonConsole.js';
-
-/**
- * Resolve `log_format` with the SAME precedence as the server's `loadConfig`
- * (index.ts): defaults from `config.default.json`, overlaid by a single
- * override file — `PUTER_CONFIG_PATH` if it exists, else `/config.json`.
- * This preload runs before the backend loads config, so it must stay in lockstep
- * with that resolution or it'd act on a value the server never sees.
- */
-const configLogFormat = (): unknown => {
- const pkgRoot = path.resolve(__dirname, '../../..');
- const readJson = (file: string): Record => {
- try {
- return JSON.parse(readFileSync(file, 'utf8'));
- } catch {
- // A missing/invalid file is treated as no override; the server's
- // loadConfig surfaces a real parse error moments later.
- return {};
- }
- };
-
- const defaultPath = path.join(pkgRoot, 'config.default.json');
- const runtimePath = path.join(pkgRoot, 'config.json');
- const envPath = process.env.PUTER_CONFIG_PATH;
-
- const defaults = existsSync(defaultPath) ? readJson(defaultPath) : {};
- const overridePath =
- envPath && existsSync(envPath)
- ? envPath
- : existsSync(runtimePath)
- ? runtimePath
- : null;
- const override = overridePath ? readJson(overridePath) : {};
-
- // deepMerge over a scalar key == override wins when it sets the key.
- return 'log_format' in override ? override.log_format : defaults.log_format;
-};
-
-// When config sets `log_format: "json"`, replace the global console so every
-// call emits one JSON line tagged with the active trace — one event per call,
-// filterable by level. Any other value (the default) leaves console untouched.
-if (configLogFormat() === 'json') {
- installJsonConsole({
- getTraceContext: () => {
- const ctx = trace.getActiveSpan()?.spanContext();
- if (!ctx || !isSpanContextValid(ctx)) return undefined;
- return { traceId: ctx.traceId, spanId: ctx.spanId };
- },
- });
-}
-
const endpoint =
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4317';
const sampleRatio = Number(process.env.OTEL_TRACE_SAMPLE_RATIO ?? 0.05);