diff --git a/src/core/tracing/TdSpanExporter.ts b/src/core/tracing/TdSpanExporter.ts index 5208669..748c939 100644 --- a/src/core/tracing/TdSpanExporter.ts +++ b/src/core/tracing/TdSpanExporter.ts @@ -102,8 +102,14 @@ export class TdSpanExporter implements SpanExporter { * Export spans using all configured adapters */ export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void { + this._exportAsync(spans).then( + () => resultCallback({ code: ExportResultCode.SUCCESS }), + (error) => resultCallback({ code: ExportResultCode.FAILED, error }), + ); + } + + private async _exportAsync(spans: ReadableSpan[]): Promise { if (this.mode !== TuskDriftMode.RECORD) { - resultCallback({ code: ExportResultCode.SUCCESS }); return; } @@ -171,29 +177,26 @@ export class TdSpanExporter implements SpanExporter { `Filtered ${filteredSpansBasedOnLibraryName.length - filteredBlockedSpans.length} blocked/oversized span(s), ${filteredBlockedSpans.length} remaining`, ); - // Transform spans to CleanSpanData - const cleanSpans: CleanSpanData[] = filteredBlockedSpans.map((span) => - SpanTransformer.transformSpanToCleanJSON(span, this.environment), - ); - - if (this.adapters.length === 0) { - logger.debug("No adapters configured"); - resultCallback({ code: ExportResultCode.SUCCESS }); - return; + // Yield the event loop between chunks to avoid blocking pool callbacks, timers, and I/O. + const TRANSFORM_CHUNK_SIZE = 20; + const cleanSpans: CleanSpanData[] = []; + for (let i = 0; i < filteredBlockedSpans.length; i += TRANSFORM_CHUNK_SIZE) { + const end = Math.min(i + TRANSFORM_CHUNK_SIZE, filteredBlockedSpans.length); + for (let j = i; j < end; j++) { + cleanSpans.push( + SpanTransformer.transformSpanToCleanJSON(filteredBlockedSpans[j], this.environment), + ); + } + if (end < filteredBlockedSpans.length) { + await new Promise((resolve) => setImmediate(resolve)); + } } - // Filter adapters based on mode - if (this.adapters.length === 0) { - logger.debug(`No active adapters for mode: ${this.mode}`); - resultCallback({ code: ExportResultCode.SUCCESS }); return; } - // Export to all active adapters - Promise.all(this.adapters.map((adapter) => adapter.exportSpans(cleanSpans))) - .then(() => resultCallback({ code: ExportResultCode.SUCCESS })) - .catch((error) => resultCallback({ code: ExportResultCode.FAILED, error })); + await Promise.all(this.adapters.map((adapter) => adapter.exportSpans(cleanSpans))); } /** diff --git a/src/core/tracing/adapters/FilesystemSpanAdapter.ts b/src/core/tracing/adapters/FilesystemSpanAdapter.ts index d282e94..1f802bb 100644 --- a/src/core/tracing/adapters/FilesystemSpanAdapter.ts +++ b/src/core/tracing/adapters/FilesystemSpanAdapter.ts @@ -1,4 +1,5 @@ import * as fs from "fs"; +import * as fsp from "fs/promises"; import * as path from "path"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; import type { SpanExportAdapter } from "../TdSpanExporter"; @@ -30,22 +31,33 @@ export class FilesystemSpanAdapter implements SpanExportAdapter { async exportSpans(spans: CleanSpanData[]): Promise { try { + // Group spans by trace file so we do one write per file instead of one per span. + const linesByFile = new Map(); + for (const span of spans) { const traceId = span.traceId; - // Get or create file path for this trace ID let filePath = this.traceFileMap.get(traceId); - if (!filePath) { const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-"); filePath = path.join(this.baseDirectory, `${isoTimestamp}_trace_${traceId}.jsonl`); this.traceFileMap.set(traceId, filePath); } - const jsonLine = JSON.stringify({ ...span, kind: mapOtToPb(span.kind as OtSpanKind) }) + "\n"; - fs.appendFileSync(filePath, jsonLine, "utf8"); + let lines = linesByFile.get(filePath); + if (!lines) { + lines = []; + linesByFile.set(filePath, lines); + } + lines.push(JSON.stringify({ ...span, kind: mapOtToPb(span.kind as OtSpanKind) })); } + await Promise.all( + Array.from(linesByFile.entries()).map(([filePath, lines]) => + fsp.appendFile(filePath, lines.join("\n") + "\n", "utf8"), + ), + ); + logger.debug( `Exported ${spans.length} span(s) to trace-specific files in ${this.baseDirectory}`, );