-
-
Save eragon512/80c671db1a77127e59be3e2ec64d83d0 to your computer and use it in GitHub Desktop.
Reads .next/trace and exports as OTLP Traces
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Post-build step: reads .next/trace and exports the | |
| * build spans as OTLP traces. | |
| * | |
| * Run after `next build` completes: | |
| * node scripts/export-build-traces.js | |
| * | |
| * Required env vars: | |
| * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | |
| * OTEL_EXPORTER_OTLP_HEADERS (comma-separated k=v) | |
| * | |
| * Optional: | |
| * TRACEPARENT - if set, spans are parented under it | |
| */ | |
| const { readFileSync } = require('node:fs'); | |
| const SERVICE_NAME = 'oodle-frontend-nextjs-build'; | |
| const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; | |
| if (!endpoint) { | |
| console.error( | |
| '[export-build-traces] ' + 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is required' | |
| ); | |
| process.exit(1); | |
| } | |
| async function main() { | |
| const { | |
| BasicTracerProvider, | |
| BatchSpanProcessor, | |
| } = require('@opentelemetry/sdk-trace-node'); | |
| const { | |
| OTLPTraceExporter, | |
| } = require('@opentelemetry/exporter-trace-otlp-http'); | |
| const { resourceFromAttributes } = require('@opentelemetry/resources'); | |
| const { trace, SpanStatusCode, ROOT_CONTEXT } = require('@opentelemetry/api'); | |
| const { W3CTraceContextPropagator } = require('@opentelemetry/core'); | |
| const rawHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS || ''; | |
| const headers = Object.fromEntries( | |
| rawHeaders | |
| .split(',') | |
| .filter(Boolean) | |
| .map((h) => { | |
| const idx = h.indexOf('='); | |
| return [h.slice(0, idx).trim(), h.slice(idx + 1).trim()]; | |
| }) | |
| ); | |
| const resource = resourceFromAttributes({ | |
| 'service.name': SERVICE_NAME, | |
| 'github.run_id': process.env.GITHUB_RUN_ID || '', | |
| 'github.sha': process.env.GITHUB_SHA || '', | |
| }); | |
| const provider = new BasicTracerProvider({ | |
| resource, | |
| spanProcessors: [ | |
| new BatchSpanProcessor( | |
| new OTLPTraceExporter({ | |
| url: endpoint, | |
| headers, | |
| }) | |
| ), | |
| ], | |
| }); | |
| const tracer = provider.getTracer(SERVICE_NAME); | |
| let parentCtx = ROOT_CONTEXT; | |
| if (process.env.TRACEPARENT) { | |
| const propagator = new W3CTraceContextPropagator(); | |
| parentCtx = propagator.extract( | |
| ROOT_CONTEXT, | |
| { traceparent: process.env.TRACEPARENT }, | |
| { | |
| get: (c, key) => c[key], | |
| keys: (c) => Object.keys(c), | |
| } | |
| ); | |
| } | |
| const traceFile = process.argv[2] || '.next/trace'; | |
| let traceEntries; | |
| try { | |
| const raw = readFileSync(traceFile, 'utf8'); | |
| traceEntries = raw | |
| .trim() | |
| .split('\n') | |
| .filter(Boolean) | |
| .flatMap((line) => { | |
| const parsed = JSON.parse(line); | |
| return Array.isArray(parsed) ? parsed : [parsed]; | |
| }); | |
| } catch (err) { | |
| console.error( | |
| `[export-build-traces] ` + `Cannot read ${traceFile}:`, | |
| err.message | |
| ); | |
| await provider.shutdown(); | |
| process.exit(1); | |
| } | |
| if (!traceEntries.length) { | |
| console.warn('[export-build-traces] ' + 'No entries in trace file'); | |
| await provider.shutdown(); | |
| process.exit(0); | |
| } | |
| const byId = new Set(traceEntries.map((e) => e.id)); | |
| const roots = []; | |
| const children = new Map(); | |
| for (const entry of traceEntries) { | |
| if (!entry.parentId || !byId.has(entry.parentId)) { | |
| roots.push(entry); | |
| } else { | |
| if (!children.has(entry.parentId)) { | |
| children.set(entry.parentId, []); | |
| } | |
| children.get(entry.parentId).push(entry); | |
| } | |
| } | |
| function createSpans(entries, parentContext) { | |
| for (const entry of entries) { | |
| const startMs = entry.startTime; | |
| const endMs = entry.startTime + entry.duration / 1000; | |
| const span = tracer.startSpan( | |
| entry.name, | |
| { | |
| startTime: startMs, | |
| attributes: entry.tags | |
| ? Object.fromEntries( | |
| Object.entries(entry.tags).map(([k, v]) => [ | |
| `next.${k}`, | |
| String(v), | |
| ]) | |
| ) | |
| : {}, | |
| }, | |
| parentContext | |
| ); | |
| span.setStatus({ | |
| code: SpanStatusCode.OK, | |
| }); | |
| span.end(endMs); | |
| const kids = children.get(entry.id); | |
| if (kids) { | |
| createSpans(kids, trace.setSpan(parentContext, span)); | |
| } | |
| } | |
| } | |
| const buildFailed = | |
| process.env.BUILD_EXIT_CODE && process.env.BUILD_EXIT_CODE !== '0'; | |
| const earliest = Math.min(...traceEntries.map((e) => e.startTime)); | |
| const latest = Math.max( | |
| ...traceEntries.map((e) => e.startTime + e.duration / 1000) | |
| ); | |
| const rootSpan = tracer.startSpan( | |
| 'next-build', | |
| { startTime: earliest }, | |
| parentCtx | |
| ); | |
| const rootCtx = trace.setSpan(parentCtx, rootSpan); | |
| createSpans(roots, rootCtx); | |
| rootSpan.setStatus( | |
| buildFailed | |
| ? { | |
| code: SpanStatusCode.ERROR, | |
| message: `next build exited ${process.env.BUILD_EXIT_CODE}`, | |
| } | |
| : { code: SpanStatusCode.OK } | |
| ); | |
| rootSpan.end(latest); | |
| await provider.forceFlush(); | |
| await provider.shutdown(); | |
| console.log( | |
| `[export-build-traces] Exported ` + | |
| `${traceEntries.length} spans to ${endpoint}` | |
| ); | |
| } | |
| main().catch((err) => { | |
| console.error('[export-build-traces] Fatal:', err); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment