autotel
Version:
Write Once, Observe Anywhere
1 lines • 15.3 kB
Source Map (JSON)
{"version":3,"file":"pretty-console-exporter-CMzlrRNg.cjs","names":["SpanStatusCode"],"sources":["../src/pretty-console-exporter.ts"],"sourcesContent":["/**\n * Pretty Console Exporter\n *\n * A developer-friendly span exporter that displays colorized, hierarchical\n * trace output in the terminal. Zero external dependencies - uses ANSI escape codes.\n *\n * @example Basic usage\n * ```typescript\n * init({\n * service: 'my-app',\n * debug: 'pretty' // Uses PrettyConsoleExporter\n * })\n * ```\n *\n * @example Explicit usage with options\n * ```typescript\n * import { PrettyConsoleExporter } from 'autotel/exporters'\n *\n * init({\n * service: 'my-app',\n * spanExporters: [new PrettyConsoleExporter({\n * colors: true,\n * showAttributes: true,\n * hideAttributes: ['http.user_agent']\n * })]\n * })\n * ```\n */\n\nimport type { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { SpanStatusCode } from '@opentelemetry/api';\n\n/**\n * Export result code constants (avoid importing @opentelemetry/core)\n */\nconst ExportResultCode = {\n SUCCESS: 0,\n FAILED: 1,\n} as const;\n\n/**\n * Export result type for SpanExporter callback\n */\ninterface ExportResult {\n code: number;\n error?: Error;\n}\n\n/**\n * ANSI escape codes for terminal colors (zero dependencies)\n */\nconst ANSI = {\n reset: '\\u001B[0m',\n bold: '\\u001B[1m',\n dim: '\\u001B[2m',\n green: '\\u001B[32m',\n red: '\\u001B[31m',\n yellow: '\\u001B[33m',\n blue: '\\u001B[34m',\n cyan: '\\u001B[36m',\n gray: '\\u001B[90m',\n} as const;\n\ntype AnsiColor = keyof typeof ANSI;\n\n/**\n * Configuration options for PrettyConsoleExporter\n */\nexport interface PrettyConsoleExporterOptions {\n /**\n * Enable ANSI colors in output\n * @default auto-detect TTY\n */\n colors?: boolean;\n\n /**\n * Show span attributes in output\n * @default true\n */\n showAttributes?: boolean;\n\n /**\n * Maximum length for attribute values before truncation\n * @default 50\n */\n maxValueLength?: number;\n\n /**\n * Show instrumentation scope name (e.g., [http], [pg])\n * @default true\n */\n showScope?: boolean;\n\n /**\n * Attribute keys to always hide from output\n * @default []\n */\n hideAttributes?: string[];\n\n /**\n * Show trace ID for each root span\n * @default false\n */\n showTraceId?: boolean;\n}\n\n/**\n * Internal node structure for building span trees\n */\ninterface SpanNode {\n span: ReadableSpan;\n children: SpanNode[];\n}\n\n/**\n * Pretty Console Exporter - colorized, hierarchical span output for development\n *\n * Features:\n * - Colorized status indicators (✓ green, ✗ red)\n * - Duration with color coding (fast=green, medium=yellow, slow=red)\n * - Hierarchical tree view showing parent-child relationships\n * - Attribute display with truncation\n * - Error message highlighting\n */\nexport class PrettyConsoleExporter implements SpanExporter {\n private readonly options: Required<PrettyConsoleExporterOptions>;\n\n constructor(options: PrettyConsoleExporterOptions = {}) {\n this.options = {\n colors: options.colors ?? process.stdout?.isTTY ?? false,\n showAttributes: options.showAttributes ?? true,\n maxValueLength: options.maxValueLength ?? 50,\n showScope: options.showScope ?? true,\n hideAttributes: options.hideAttributes ?? [],\n showTraceId: options.showTraceId ?? false,\n };\n }\n\n /**\n * Export spans with pretty formatting\n */\n export(\n spans: ReadableSpan[],\n resultCallback: (result: ExportResult) => void,\n ): void {\n if (spans.length === 0) {\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n\n try {\n // Group spans by trace ID\n const traceGroups = this.groupByTrace(spans);\n\n // Print each trace group\n for (const [traceId, traceSpans] of traceGroups) {\n this.printTrace(traceId, traceSpans);\n }\n\n resultCallback({ code: ExportResultCode.SUCCESS });\n } catch {\n // Fail-open: don't crash the app if formatting fails\n resultCallback({ code: ExportResultCode.SUCCESS });\n }\n }\n\n /**\n * Group spans by their trace ID\n */\n private groupByTrace(spans: ReadableSpan[]): Map<string, ReadableSpan[]> {\n const groups = new Map<string, ReadableSpan[]>();\n\n for (const span of spans) {\n const traceId = span.spanContext().traceId;\n const group = groups.get(traceId) ?? [];\n group.push(span);\n groups.set(traceId, group);\n }\n\n return groups;\n }\n\n /**\n * Print a single trace with all its spans as a tree\n */\n private printTrace(traceId: string, spans: ReadableSpan[]): void {\n // Sort by start time\n const sorted = [...spans].toSorted((a, b) => {\n const aTime = hrTimeToMs(a.startTime);\n const bTime = hrTimeToMs(b.startTime);\n return aTime - bTime;\n });\n\n // Build tree structure\n const tree = this.buildSpanTree(sorted);\n\n // Print trace ID header if enabled\n if (this.options.showTraceId && tree.length > 0) {\n console.log(this.color(`trace: ${traceId}`, 'gray'));\n }\n\n // Print each root span and its children\n for (const node of tree) {\n this.printNode(node, 0, false);\n }\n\n // Add blank line between traces\n console.log('');\n }\n\n /**\n * Build a tree structure from flat spans using parent-child relationships\n */\n private buildSpanTree(spans: ReadableSpan[]): SpanNode[] {\n const spanMap = new Map<string, SpanNode>();\n const roots: SpanNode[] = [];\n\n // Create nodes for all spans\n for (const span of spans) {\n const spanId = span.spanContext().spanId;\n spanMap.set(spanId, { span, children: [] });\n }\n\n // Build parent-child relationships\n for (const span of spans) {\n const spanId = span.spanContext().spanId;\n const parentId = span.parentSpanContext?.spanId;\n const node = spanMap.get(spanId)!;\n\n if (parentId && spanMap.has(parentId)) {\n // Has parent in this batch - add as child\n spanMap.get(parentId)!.children.push(node);\n } else {\n // No parent or parent not in batch - treat as root\n roots.push(node);\n }\n }\n\n return roots;\n }\n\n /**\n * Print a span node with indentation and tree characters\n */\n private printNode(node: SpanNode, depth: number, isLast: boolean): void {\n const { span } = node;\n\n // Build tree prefix\n const prefix =\n depth === 0 ? '' : ' '.repeat(depth - 1) + (isLast ? '└─ ' : '├─ ');\n\n // Status indicator\n const isError = span.status.code === SpanStatusCode.ERROR;\n const statusChar = isError ? '✗' : '✓';\n const statusColor: AnsiColor = isError ? 'red' : 'green';\n\n // Duration formatting\n const durationMs = hrTimeToMs(span.duration);\n const durationStr = formatDuration(durationMs);\n const durationColor = getDurationColor(durationMs);\n\n // Scope name (instrumentation library)\n const scopeName = this.options.showScope\n ? this.color(` [${this.getScopeName(span)}]`, 'gray')\n : '';\n\n // Build the main line\n const line = [\n prefix,\n this.color(statusChar, statusColor),\n ' ',\n span.name.padEnd(Math.max(35 - prefix.length, 10)),\n this.color(durationStr.padStart(8), durationColor),\n scopeName,\n ].join('');\n\n console.log(line);\n\n // Print attributes on next line (indented)\n if (this.options.showAttributes) {\n const attrs = this.formatAttributes(span);\n if (attrs) {\n const attrIndent = ' '.repeat(depth) + ' ';\n console.log(this.color(`${attrIndent}${attrs}`, 'dim'));\n }\n }\n\n // Print error message if present\n if (isError && span.status.message) {\n const errorIndent = ' '.repeat(depth) + ' ';\n console.log(\n this.color(`${errorIndent}Error: ${span.status.message}`, 'red'),\n );\n }\n\n // Print children\n const childCount = node.children.length;\n let index = 0;\n for (const child of node.children) {\n this.printNode(child, depth + 1, index === childCount - 1);\n index++;\n }\n }\n\n /**\n * Get short scope name from instrumentation scope\n */\n private getScopeName(span: ReadableSpan): string {\n const name = span.instrumentationScope?.name ?? 'unknown';\n // Extract short name from @opentelemetry/instrumentation-xxx format\n const match = name.match(/@opentelemetry\\/instrumentation-(.+)/);\n if (match?.[1]) return match[1];\n // Fall back to last part of name or full name\n const lastPart = name.split('/').at(-1);\n return lastPart ?? name;\n }\n\n /**\n * Format span attributes as a comma-separated string\n */\n private formatAttributes(span: ReadableSpan): string {\n const attrs = span.attributes;\n if (!attrs || Object.keys(attrs).length === 0) {\n return '';\n }\n\n const pairs: string[] = [];\n for (const [key, value] of Object.entries(attrs)) {\n // Skip hidden attributes\n if (this.options.hideAttributes.includes(key)) continue;\n\n // Skip undefined/null values\n if (value === undefined || value === null) continue;\n\n // Format value\n const strValue = this.truncate(\n Array.isArray(value) ? `[${value.join(', ')}]` : String(value),\n this.options.maxValueLength,\n );\n pairs.push(`${key}=${strValue}`);\n }\n\n return pairs.join(', ');\n }\n\n /**\n * Truncate string to max length with ellipsis\n */\n private truncate(str: string, max: number): string {\n if (str.length <= max) return str;\n return str.slice(0, max - 3) + '...';\n }\n\n /**\n * Apply ANSI color if colors are enabled\n */\n private color(text: string, color: AnsiColor): string {\n if (!this.options.colors) return text;\n return `${ANSI[color]}${text}${ANSI.reset}`;\n }\n\n /**\n * Shutdown (no-op for console exporter)\n */\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Force flush (no-op for console exporter)\n */\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/**\n * Convert HrTime [seconds, nanoseconds] to milliseconds\n */\nfunction hrTimeToMs(hrTime: [number, number]): number {\n const [seconds, nanos] = hrTime;\n return seconds * 1000 + nanos / 1_000_000;\n}\n\n/**\n * Format duration with appropriate units\n */\nfunction formatDuration(ms: number): string {\n if (ms < 1) {\n // Sub-millisecond: show as microseconds\n return `${(ms * 1000).toFixed(0)}µs`;\n }\n if (ms < 1000) {\n // Under 1 second: show as milliseconds\n return `${ms.toFixed(0)}ms`;\n }\n // 1 second or more: show as seconds\n return `${(ms / 1000).toFixed(2)}s`;\n}\n\n/**\n * Get color based on duration (fast=green, medium=yellow, slow=red)\n */\nfunction getDurationColor(ms: number): AnsiColor {\n if (ms < 100) return 'green';\n if (ms < 500) return 'yellow';\n return 'red';\n}\n\n/**\n * Export utility functions for testing\n */\nexport { formatDuration, getDurationColor, hrTimeToMs };\n"],"mappings":"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"}