diff --git a/packages/core/src/telemetry/sdk.test.ts b/packages/core/src/telemetry/sdk.test.ts index d25bd688e3d..90b7a98ff37 100644 --- a/packages/core/src/telemetry/sdk.test.ts +++ b/packages/core/src/telemetry/sdk.test.ts @@ -24,6 +24,11 @@ import { TelemetryTarget } from './index.js'; import * as os from 'node:os'; import * as path from 'node:path'; +import { promises as fs } from 'node:fs'; +import { + resetDebugLoggingState, + setDebugLogSession, +} from '../utils/debugLogger.js'; vi.mock('@opentelemetry/exporter-trace-otlp-grpc'); vi.mock('@opentelemetry/exporter-logs-otlp-grpc'); @@ -143,6 +148,77 @@ describe('Telemetry SDK', () => { ); }); + it('should route OpenTelemetry diagnostics to debug log instead of console output', async () => { + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + const consoleWarnSpy = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}); + const mkdirSpy = vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined); + const appendFileSpy = vi + .spyOn(fs, 'appendFile') + .mockResolvedValue(undefined); + const unlinkSpy = vi.spyOn(fs, 'unlink').mockResolvedValue(undefined); + const symlinkSpy = vi.spyOn(fs, 'symlink').mockResolvedValue(undefined); + const previousDebugLogFileEnv = process.env['QWEN_DEBUG_LOG_FILE']; + try { + process.env['QWEN_DEBUG_LOG_FILE'] = '1'; + setDebugLogSession({ getSessionId: () => 'otel-diag-test-session' }); + + diag.error( + JSON.stringify({ + message: + 'Error: PeriodicExportingMetricReader: metrics export failed (error Error: connect ECONNREFUSED)', + }), + ); + + diag.error('A different OpenTelemetry diagnostic'); + diag.warn('An OpenTelemetry warning'); + + await vi.waitFor(() => { + expect(appendFileSpy).toHaveBeenCalledTimes(3); + }); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(mkdirSpy).toHaveBeenCalled(); + expect(appendFileSpy).toHaveBeenCalledWith( + expect.stringContaining('otel-diag-test-session'), + expect.stringContaining( + '[ERROR] [OTEL] {"message":"Error: PeriodicExportingMetricReader: metrics export failed (error Error: connect ECONNREFUSED)"}', + ), + 'utf8', + ); + expect(appendFileSpy).toHaveBeenCalledWith( + expect.stringContaining('otel-diag-test-session'), + expect.stringContaining( + '[ERROR] [OTEL] A different OpenTelemetry diagnostic', + ), + 'utf8', + ); + expect(appendFileSpy).toHaveBeenCalledWith( + expect.stringContaining('otel-diag-test-session'), + expect.stringContaining('[WARN] [OTEL] An OpenTelemetry warning'), + 'utf8', + ); + } finally { + consoleErrorSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + mkdirSpy.mockRestore(); + appendFileSpy.mockRestore(); + unlinkSpy.mockRestore(); + symlinkSpy.mockRestore(); + setDebugLogSession(null); + resetDebugLoggingState(); + if (previousDebugLogFileEnv === undefined) { + delete process.env['QWEN_DEBUG_LOG_FILE']; + } else { + process.env['QWEN_DEBUG_LOG_FILE'] = previousDebugLogFileEnv; + } + } + }); + it('should use HTTP exporters with signal-specific paths when protocol is http', () => { vi.spyOn(mockConfig, 'getTelemetryEnabled').mockReturnValue(true); vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http'); diff --git a/packages/core/src/telemetry/sdk.ts b/packages/core/src/telemetry/sdk.ts index 74c26b31754..4c47a02f2c0 100644 --- a/packages/core/src/telemetry/sdk.ts +++ b/packages/core/src/telemetry/sdk.ts @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api'; +import { DiagLogLevel, diag } from '@opentelemetry/api'; +import type { DiagLogger } from '@opentelemetry/api'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc'; import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc'; @@ -30,8 +31,21 @@ import { import { createDebugLogger } from '../utils/debugLogger.js'; import { LogToSpanProcessor } from './log-to-span-processor.js'; -// For troubleshooting, set the log level to DiagLogLevel.DEBUG -diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN); +function createTelemetryDiagLogger(): DiagLogger { + const debugLogger = createDebugLogger('OTEL'); + return { + error: (message, ...args) => debugLogger.error(message, ...args), + warn: (message, ...args) => debugLogger.warn(message, ...args), + info: (message, ...args) => debugLogger.info(message, ...args), + debug: (message, ...args) => debugLogger.debug(message, ...args), + verbose: (message, ...args) => debugLogger.debug(message, ...args), + }; +} + +// For troubleshooting, set the log level to DiagLogLevel.DEBUG. +// OTel SDK diagnostics must not write to console because console output can be +// surfaced in user-visible UI. Keep diagnostics in the debug log instead. +diag.setLogger(createTelemetryDiagLogger(), DiagLogLevel.WARN); /** * Standard OTLP HTTP signal-specific paths per the OpenTelemetry specification.