diff --git a/index.ts b/index.ts index 82c8bc3..2e3c3a8 100644 --- a/index.ts +++ b/index.ts @@ -2,3 +2,4 @@ export { fetchGcpProjectId } from "./lib/gcp"; export { getHttpTraceHeader } from "./lib/http"; export { getLoggingTraceData, logger } from "./lib/logging"; export { middleware } from "./lib/middleware"; +export { createTraceparent } from "./lib/traceparent"; diff --git a/lib/middleware.ts b/lib/middleware.ts index cf64a01..5073a46 100644 --- a/lib/middleware.ts +++ b/lib/middleware.ts @@ -1,5 +1,6 @@ import type { RequestHandler } from "express"; import { AsyncLocalStorage } from "node:async_hooks"; +import { createTraceparent } from "./traceparent"; type Store = { traceparent?: string; @@ -10,7 +11,7 @@ type Store = { const storage = new AsyncLocalStorage(); export const middleware: RequestHandler = (req, _res, next) => { - const traceparent = req.header("traceparent"); + const traceparent = req.header("traceparent") || createTraceparent(); storage.run({ traceparent }, () => { next(); diff --git a/lib/traceparent.ts b/lib/traceparent.ts index f11acc6..7780e3f 100644 --- a/lib/traceparent.ts +++ b/lib/traceparent.ts @@ -1,3 +1,5 @@ +import crypto from "crypto"; + /** * Traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00 * @@ -17,3 +19,12 @@ export function getTraceFromTraceparent(traceHeader: string) { isSampled: parts[3] !== "00", }; } + +export function createTraceparent(isSampled: boolean = false) { + const version = "00"; + const traceId = crypto.randomBytes(16).toString("hex"); + const parentId = crypto.randomBytes(8).toString("hex"); + const flags = isSampled ? "01" : "00"; + + return `${version}-${traceId}-${parentId}-${flags}`; +} diff --git a/test/unit/middleware.test.ts b/test/unit/middleware.test.ts index c2dd2a0..31df15a 100644 --- a/test/unit/middleware.test.ts +++ b/test/unit/middleware.test.ts @@ -11,6 +11,16 @@ describe("Express middleware", () => { }); }); + it("should create a new traceparent and store it in the store if no traceparent header was provided", () => { + const req = { header: () => {} }; + + // @ts-expect-error - We don't need the full Express Request object + middleware(req, {}, () => { + const traceparent = getStore().traceparent || ""; + expect(new RegExp(/^[\da-f-]{55}$/).test(traceparent)).to.equal(true); + }); + }); + it("should return an empty object if the middleware is not used", () => { expect(getStore()).to.deep.equal({}); }); diff --git a/test/unit/traceparent.test.ts b/test/unit/traceparent.test.ts index e9fe8ac..e77c36f 100644 --- a/test/unit/traceparent.test.ts +++ b/test/unit/traceparent.test.ts @@ -1,4 +1,4 @@ -import { getTraceFromTraceparent } from "../../lib/traceparent"; +import { getTraceFromTraceparent, createTraceparent } from "../../lib/traceparent"; describe("Traceparent parsing", () => { it("should parse a traceparent header correctly", async () => { @@ -21,3 +21,25 @@ describe("Traceparent parsing", () => { expect(trace).to.equal(undefined); }); }); + +describe("Traceparent creation", () => { + it("should create a traceparent header correctly", async () => { + const traceparent = createTraceparent(); + const [version, traceId, parentId, isSampled] = traceparent.split("-"); + + expect(version).to.equal("00"); // we only support version 00 + expect(isSampled).to.equal("00"); // default is not sampled + + expect(new RegExp(/^[\da-f]{2}$/).test(version)).to.equal(true, version); + expect(new RegExp(/^[\da-f]{32}$/).test(traceId)).to.equal(true, traceId); + expect(new RegExp(/^[\da-f]{16}$/).test(parentId)).to.equal(true, parentId); + expect(new RegExp(/^[\da-f]{2}$/).test(isSampled)).to.equal(true, isSampled); + }); + + it("Sets flags if passed isSampled true", async () => { + const traceparent = createTraceparent(true); + const [, , , isSampled] = traceparent.split("-"); + + expect(isSampled).to.equal("01"); + }); +});