mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
fix(telemetry): use safeJsonStringify in FileExporter to avoid circular reference crash (#3630)
When --telemetry-outfile is configured, FileSpanExporter.serialize called JSON.stringify directly on OTel ReadableSpan instances. The spans hold a back-reference to BatchSpanProcessor (._shutdownOnce -> BindOnceFuture._that -> BatchSpanProcessor), which forms a cycle and triggers "TypeError: Converting circular structure to JSON" on every export. Combined with DiagConsoleLogger, the error was repeatedly printed to stderr and polluted the Ink TUI. Switch FileExporter.serialize to the existing safeJsonStringify utility, matching the upstream gemini-cli fix so future merges stay clean. Add a focused regression test that mimics the BatchSpanProcessor cycle shape; broader cycle behavior is already covered by safeJsonStringify.test.ts. Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
This commit is contained in:
parent
eea4e10eea
commit
569cfe10fa
2 changed files with 52 additions and 1 deletions
50
packages/core/src/telemetry/file-exporters.test.ts
Normal file
50
packages/core/src/telemetry/file-exporters.test.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { FileSpanExporter } from './file-exporters.js';
|
||||||
|
|
||||||
|
type SerializeAccess = { serialize: (data: unknown) => string };
|
||||||
|
|
||||||
|
describe('FileExporter.serialize', () => {
|
||||||
|
let tmpDir: string;
|
||||||
|
let exporter: FileSpanExporter;
|
||||||
|
let serialize: (data: unknown) => string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'file-exporters-test-'));
|
||||||
|
exporter = new FileSpanExporter(path.join(tmpDir, 'out.jsonl'));
|
||||||
|
serialize = (exporter as unknown as SerializeAccess).serialize.bind(
|
||||||
|
exporter,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await exporter.shutdown();
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Regression for upstream PR #4689: a raw JSON.stringify on a ReadableSpan
|
||||||
|
// crashed because BatchSpanProcessor._shutdownOnce -> BindOnceFuture._that
|
||||||
|
// forms a cycle. The exporter must delegate to safeJsonStringify so cycles
|
||||||
|
// become "[Circular]" instead of throwing.
|
||||||
|
it('does not throw on BatchSpanProcessor-shaped cycle', () => {
|
||||||
|
const proc: Record<string, unknown> = { kind: 'BatchSpanProcessor' };
|
||||||
|
const future: Record<string, unknown> = { kind: 'BindOnceFuture' };
|
||||||
|
proc['_shutdownOnce'] = future;
|
||||||
|
future['_that'] = proc;
|
||||||
|
const span = { name: 'span-1', _spanProcessor: proc };
|
||||||
|
|
||||||
|
expect(() => serialize(span)).not.toThrow();
|
||||||
|
const out = serialize(span);
|
||||||
|
expect(out).toContain('"name": "span-1"');
|
||||||
|
expect(out).toContain('"[Circular]"');
|
||||||
|
expect(out.endsWith('\n')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -17,6 +17,7 @@ import type {
|
||||||
PushMetricExporter,
|
PushMetricExporter,
|
||||||
} from '@opentelemetry/sdk-metrics';
|
} from '@opentelemetry/sdk-metrics';
|
||||||
import { AggregationTemporality } from '@opentelemetry/sdk-metrics';
|
import { AggregationTemporality } from '@opentelemetry/sdk-metrics';
|
||||||
|
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||||
|
|
||||||
class FileExporter {
|
class FileExporter {
|
||||||
protected writeStream: fs.WriteStream;
|
protected writeStream: fs.WriteStream;
|
||||||
|
|
@ -26,7 +27,7 @@ class FileExporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected serialize(data: unknown): string {
|
protected serialize(data: unknown): string {
|
||||||
return JSON.stringify(data, null, 2) + '\n';
|
return safeJsonStringify(data, 2) + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown(): Promise<void> {
|
shutdown(): Promise<void> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue