From 3d6453670570a63c6a4d1d51f2ce41b11c8250d4 Mon Sep 17 00:00:00 2001 From: Daniel Climent Date: Thu, 26 Oct 2023 09:28:15 +0200 Subject: [PATCH] feat: Add support for loading a tracing baggage dump in WC3 Baggage format (#5343) * feat: add support for loading a tracing baggage dump in wc3 baggage format * feat: expose flag to set baggageFilePath * fix: set ctx after loadBaggageFile * chore: move context around and change to readFileSync * fix: update tests after context changes and sync file reading * fix: update tests with PR review comments * fix: fix variable shadowing --------- Co-authored-by: JGAntunes --- package-lock.json | 23 ++++++ packages/build/package.json | 1 + packages/build/src/core/flags.js | 5 ++ packages/build/src/core/normalize_flags.ts | 1 + packages/build/src/core/types.ts | 1 + packages/build/src/tracing/main.ts | 35 +++++++-- packages/build/tests/tracing/tests.js | 84 +++++++++++++++++++++- 7 files changed, 144 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 950331950a..fa6456baeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26007,6 +26007,7 @@ "@netlify/run-utils": "^5.1.1", "@netlify/zip-it-and-ship-it": "9.25.4", "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.17.1", "@sindresorhus/slugify": "^2.0.0", "ansi-escapes": "^6.0.0", "chalk": "^5.0.0", @@ -26305,6 +26306,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/build/node_modules/@opentelemetry/core": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.17.1.tgz", + "integrity": "sha512-I6LrZvl1FF97FQXPR0iieWQmKnGxYtMbWA1GrAXnLUR+B1Hn2m8KqQNEIlZAucyv00GBgpWkpllmULmZfG8P3g==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.17.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.7.0" + } + }, + "packages/build/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.1.tgz", + "integrity": "sha512-xbR2U+2YjauIuo42qmE8XyJK6dYeRMLJuOlUP5SO4auET4VtOHOzgkRVOq+Ik18N+Xf3YPcqJs9dZMiDddz1eQ==", + "engines": { + "node": ">=14" + } + }, "packages/build/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", diff --git a/packages/build/package.json b/packages/build/package.json index 2e675b0a9e..f35399c409 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -76,6 +76,7 @@ "@netlify/run-utils": "^5.1.1", "@netlify/zip-it-and-ship-it": "9.25.4", "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.17.1", "@sindresorhus/slugify": "^2.0.0", "ansi-escapes": "^6.0.0", "chalk": "^5.0.0", diff --git a/packages/build/src/core/flags.js b/packages/build/src/core/flags.js index 6f9521345f..268ae93eac 100644 --- a/packages/build/src/core/flags.js +++ b/packages/build/src/core/flags.js @@ -244,6 +244,11 @@ Default: false`, describe: 'Trace flags containing the trace settings for the given trace ID', hidden: true, }, + 'tracing.baggageFilePath': { + string: true, + describe: '', + hidden: true, + }, offline: { boolean: true, describe: `Do not send requests to the Netlify API to retrieve site settings. diff --git a/packages/build/src/core/normalize_flags.ts b/packages/build/src/core/normalize_flags.ts index 6b9925d326..e736f60f6c 100644 --- a/packages/build/src/core/normalize_flags.ts +++ b/packages/build/src/core/normalize_flags.ts @@ -106,6 +106,7 @@ const getDefaultFlags = function ({ env: envOpt = {} }, combinedEnv) { sampleRate: 1, httpProtocol: DEFAULT_OTEL_ENDPOINT_PROTOCOL, port: DEFAULT_OTEL_TRACING_PORT, + baggageFilePath: '', }, timeline: 'build', quiet: false, diff --git a/packages/build/src/core/types.ts b/packages/build/src/core/types.ts index 3594f940fa..3df505424e 100644 --- a/packages/build/src/core/types.ts +++ b/packages/build/src/core/types.ts @@ -86,4 +86,5 @@ export type TracingOptions = { traceId: string traceFlags: number parentSpanId: string + baggageFilePath: string } diff --git a/packages/build/src/tracing/main.ts b/packages/build/src/tracing/main.ts index c15fad64c8..dc41f941a3 100644 --- a/packages/build/src/tracing/main.ts +++ b/packages/build/src/tracing/main.ts @@ -1,5 +1,8 @@ +import { readFileSync } from 'node:fs' + import { HoneycombSDK } from '@honeycombio/opentelemetry-node' import { context, trace, propagation, SpanStatusCode, diag, DiagLogLevel, DiagLogger } from '@opentelemetry/api' +import { parseKeyPairsIntoRecord } from '@opentelemetry/core/build/src/baggage/utils.js' import { NodeSDK } from '@opentelemetry/sdk-node' import type { TracingOptions } from '../core/types.js' @@ -44,14 +47,19 @@ export const startTracing = function (options: TracingOptions, logger: (...args: sdk.start() + // Loads the contents of the passed baggageFilePath into the baggage + const baggageCtx = loadBaggageFromFile(options.baggageFilePath) + // Sets the current trace ID and span ID based on the options received // this is used as a way to propagate trace context from Buildbot - return trace.setSpanContext(context.active(), { + const ctx = trace.setSpanContext(baggageCtx, { traceId: options.traceId, spanId: options.parentSpanId, traceFlags: options.traceFlags, isRemote: true, }) + + return ctx } /** Stops the tracing service if there's one running. This will flush any ongoing events */ @@ -67,13 +75,15 @@ export const stopTracing = async function () { } } -/** Sets attributes to be propagated across child spans under the current context */ +/** Sets attributes to be propagated across child spans under the current active context */ export const setMultiSpanAttributes = function (attributes: { [key: string]: string }) { const currentBaggage = propagation.getBaggage(context.active()) + // Create a baggage if there's none let baggage = currentBaggage === undefined ? propagation.createBaggage() : currentBaggage - Object.entries(attributes).forEach((entry) => { - baggage = baggage.setEntry(entry[0], { value: entry[1] }) + Object.entries(attributes).forEach(([key, value]) => { + baggage = baggage.setEntry(key, { value }) }) + return propagation.setBaggage(context.active(), baggage) } @@ -89,6 +99,23 @@ export const addErrorToActiveSpan = function (error: Error) { }) } +//** Loads the baggage attributes from a baggabe file which follows W3C Baggage specification */ +export const loadBaggageFromFile = function (baggageFilePath: string) { + if (baggageFilePath.length === 0) { + diag.warn('Empty baggage file path provided, no context loaded') + return context.active() + } + let baggageString: string + try { + baggageString = readFileSync(baggageFilePath, 'utf-8') + } catch (error) { + diag.error(error) + return context.active() + } + const parsedBaggage = parseKeyPairsIntoRecord(baggageString) + return setMultiSpanAttributes(parsedBaggage) +} + /** Attributes used for the root span of our execution */ export type RootExecutionAttributes = { 'build.id': string diff --git a/packages/build/tests/tracing/tests.js b/packages/build/tests/tracing/tests.js index 01d43fbd03..ac7953c42c 100644 --- a/packages/build/tests/tracing/tests.js +++ b/packages/build/tests/tracing/tests.js @@ -1,8 +1,12 @@ -import { trace, TraceFlags } from '@opentelemetry/api' +import { writeFile, rm, mkdtemp } from 'fs/promises' +import { tmpdir } from 'os' +import { join } from 'path' + +import { trace, TraceFlags, propagation } from '@opentelemetry/api' import { getBaggage } from '@opentelemetry/api/build/src/baggage/context-helpers.js' import test from 'ava' -import { setMultiSpanAttributes, startTracing, stopTracing } from '../../lib/tracing/main.js' +import { setMultiSpanAttributes, startTracing, stopTracing, loadBaggageFromFile } from '../../lib/tracing/main.js' test('Tracing set multi span attributes', async (t) => { const ctx = setMultiSpanAttributes({ some: 'test', foo: 'bar' }) @@ -11,6 +15,81 @@ test('Tracing set multi span attributes', async (t) => { t.is(baggage.getEntry('foo').value, 'bar') }) +const testMatrixBaggageFile = [ + { + description: 'when baggageFilePath is blank', + input: { + baggageFilePath: '', + baggageFileContent: null, + }, + expects: { + somefield: undefined, + foo: undefined, + }, + }, + { + description: 'when baggageFilePath is set but file is empty', + input: { + baggageFilePath: 'baggage.dump', + baggageFileContent: '', + }, + expects: { + somefield: undefined, + foo: undefined, + }, + }, + { + description: 'when baggageFilePath is set and has content', + input: { + baggageFilePath: 'baggage.dump', + baggageFileContent: 'somefield=value,foo=bar', + }, + expects: { + somefield: { value: 'value' }, + foo: { value: 'bar' }, + }, + }, +] + +let baggagePath +test.before(async () => { + baggagePath = await mkdtemp(join(tmpdir(), 'baggage-path-')) +}) + +test.after(async () => { + await rm(baggagePath, { recursive: true }) +}) + +testMatrixBaggageFile.forEach((testCase) => { + test.serial(`Tracing baggage loading - ${testCase.description}`, async (t) => { + const { input, expects } = testCase + + // We only want to write the file if it's a non-empty string '', while we still want to test scenario + let filePath = input.baggageFilePath + if (input.baggageFilePath.length > 0) { + filePath = `${baggagePath}/${input.baggageFilePath}` + await writeFile(filePath, input.baggageFileContent) + } + + const ctx = loadBaggageFromFile(filePath) + const baggage = propagation.getBaggage(ctx) + + // When there's no file we test that baggage is not set + if (input.baggageFilePath === '') { + t.is(baggage, undefined) + return + } + + Object.entries(expects).forEach(([property, expected]) => { + if (expected === undefined) { + t.is(baggage.getEntry(property), expected) + } else { + t.is(baggage.getEntry(property).value, expected.value) + } + }) + }) +}) + const spanId = '6e0c63257de34c92' // The sampler is deterministic, meaning that for a given traceId it will always produce a `SAMPLED` or a `NONE` // consistently. More info in - https://opentelemetry.io/docs/specs/otel/trace/tracestate-probability-sampling/#consistent-probability-sampling @@ -78,6 +157,7 @@ testMatrix.forEach((testCase) => { traceId: input.traceId, traceFlags: input.traceFlags, parentSpanId: spanId, + baggageFilePath: '', }, noopLogger, )