@diveinto/obs 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Spurin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @diveinto/obs
2
+
3
+ Structured logging, tracing and metrics for Next.js and Node services. One
4
+ record shape, written to stdout and to two rotating files, with W3C trace
5
+ context threaded through it, and optional OpenTelemetry export that stays off
6
+ until you configure an endpoint.
7
+
8
+ ```bash
9
+ npm install @diveinto/obs
10
+ ```
11
+
12
+ ## Why it exists
13
+
14
+ It grew out of consolidating four hand-maintained copies of the same library
15
+ across four services. Copies drift: by the time they were merged, one had
16
+ real OpenTelemetry spans around requests and the others did not, one had
17
+ route templating that kept metric cardinality bounded, one had fixed a
18
+ duration that went unrecorded when something threw a non-`Error`, and one had
19
+ made log-file resolution a pure, testable function. This package is the union
20
+ of the four, and each of those behaviours is pinned by a test.
21
+
22
+ ## What you get
23
+
24
+ - **One record shape**, frozen, identical in the text and JSON renderers:
25
+ `ts level trace_id span_id parent_span_id logger event msg` plus context.
26
+ A sibling Python implementation writes the same shape, so one `grep` can
27
+ follow a trace across services written in either language.
28
+ - **Three sinks**: stdout (always), and two rotating files that are
29
+ format-locked, so `<stem>.log` is always text and `<stem>.jsonl` is always
30
+ JSON regardless of `LOG_FORMAT`. Daily rotation, 14 kept, both configurable.
31
+ - **Trace context** on `AsyncLocalStorage`, continuing an inbound W3C
32
+ `traceparent` and propagating it outbound, so one `trace_id` spans every
33
+ hop.
34
+ - **Optional OTLP export** to any OpenTelemetry collector. Off unless
35
+ `OTEL_EXPORTER_OTLP_ENDPOINT` is set, and the first-party logs never depend
36
+ on it.
37
+ - **Serverless-aware**: on Vercel or Lambda the file sinks switch themselves
38
+ off rather than failing, because there is no persistent writable disk.
39
+
40
+ ## Usage
41
+
42
+ Once, from the app's `instrumentation.ts`, on the Node runtime:
43
+
44
+ ```ts
45
+ export async function register() {
46
+ if (process.env.NEXT_RUNTIME !== "nodejs") return;
47
+
48
+ // Optional: start OTLP export first, so the earliest logs adopt OTel ids.
49
+ const { startOtel } = await import("@diveinto/obs/start");
50
+ await startOtel("my-service");
51
+
52
+ const { createObs } = await import("@diveinto/obs");
53
+ createObs({ service: "my-service", envPrefix: "MYSVC" });
54
+ }
55
+ ```
56
+
57
+ Everywhere else, plain named imports:
58
+
59
+ ```ts
60
+ import { getLogger, withApiTrace, withAction, traceHeaders } from "@diveinto/obs";
61
+
62
+ const log = getLogger("billing.invoice");
63
+
64
+ export const GET = (req: Request) =>
65
+ withApiTrace(req, async () => {
66
+ log.info("invoice.fetch", "fetching invoice", { invoice_id: id });
67
+ const res = await fetch(url, { headers: traceHeaders() }); // same trace, next hop
68
+ return Response.json(await res.json());
69
+ });
70
+ ```
71
+
72
+ `withApiTrace` opens or continues the trace, logs `request.received` and
73
+ `request.completed`, records a duration histogram, and sets `X-Trace-Id` on
74
+ the response. `withAction` does the same for a server action in a fresh root
75
+ trace.
76
+
77
+ ## Configuration
78
+
79
+ | field | required | what it does |
80
+ |---|---|---|
81
+ | `service` | yes | The OpenTelemetry tracer and meter name, the `service.name` fallback, and the default var dir and file stem |
82
+ | `envPrefix` | yes | The prefix on this service's own env vars. `MYSVC` reads `MYSVC_LOG_TEXT_FILE`, `MYSVC_VAR_DIR` and the rest, so a service adopting the library keeps the env file it already has |
83
+ | `defaultVarDir` | no | State dir when `<PREFIX>_VAR_DIR` is unset. Default `/var/lib/<service>` |
84
+ | `fileStem` | no | Stem for the two log files. Default: the last dash-separated word of `service` |
85
+ | `routeTemplate` | no | Collapse dynamic path segments before a path becomes a metric label: `/orders/abc123` to `/orders/{id}`. Without it every id is its own time series |
86
+ | `traceSeed` | no | Where a request's trace continues from when there is no W3C `traceparent`, for a front end that passes a correlation id of its own |
87
+ | `quietPathPrefixes` | no | Request paths whose completion logs at debug rather than info. Health checks and internal polling otherwise bury what matters |
88
+
89
+ ### Environment
90
+
91
+ Generic, never prefixed: `LOG_LEVEL` (`trace`/`debug`/`info`/`warn`/`error`/
92
+ `fatal`), `LOG_FORMAT` (`pretty` or `json`, stdout only), and the standard
93
+ `OTEL_*` set.
94
+
95
+ Per service, prefixed: `<PREFIX>_VAR_DIR`, `<PREFIX>_LOG_TEXT_FILE`,
96
+ `_TEXT_ROTATION`, `_TEXT_RETENTION`, and the same three for `_JSON_`. A
97
+ `*_FILE` value is a path, or `1` for the default path, or `0`/`off`.
98
+
99
+ ## Cardinality
100
+
101
+ Metric attributes become a separate time series per distinct value, so they
102
+ must be low cardinality: an outcome, a status class, a route **template**.
103
+ Never a user id, order id or request id. Those belong on spans and logs,
104
+ where the point is finding the one bad request, and are toxic on metrics.
105
+ `routeTemplate` exists for exactly this reason.
106
+
107
+ The library registers no metric reader, so metrics are no-ops unless you wire
108
+ one deliberately. That is a scar: auto-exporting metrics alongside traces
109
+ once produced tens of millions of samples a month that nobody read.
110
+
111
+ ## Requirements
112
+
113
+ Node 22 or newer, ESM. `@opentelemetry/api` is a peer dependency; the SDK
114
+ packages are optional peers, needed only if you use OTLP export.
115
+
116
+ ## License
117
+
118
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { Fields, LevelInput } from "./types.js";
2
+ /**
3
+ * Emit one named story beat.
4
+ *
5
+ * `event` is the machine-stable key (e.g. "lab.launch.started"); `message` is
6
+ * the human sentence; `fields` carry the salient facts (ids, counts, durations,
7
+ * the actual target). Use it at meaningful seams.
8
+ */
9
+ export declare function logEvent(event: string, message?: string, fields?: Fields, opts?: {
10
+ level?: LevelInput;
11
+ logger?: string;
12
+ error?: unknown;
13
+ }): void;
14
+ type SpanOpts = {
15
+ fields?: Fields;
16
+ logger?: string;
17
+ level?: LevelInput;
18
+ };
19
+ /**
20
+ * Open a child span around a unit of work: mints a child span id, binds
21
+ * `fields` as baggage (so they appear on every line inside), times the body,
22
+ * and logs `<name>.start` / `<name>.finish` (with dur_ms). On a throw it logs
23
+ * `<name>.error` with the stack and re-raises. The parent context is restored
24
+ * automatically when the body settles.
25
+ */
26
+ export declare function withSpan<T>(name: string, fn: () => Promise<T> | T, opts?: SpanOpts): Promise<T>;
27
+ /**
28
+ * Like withSpan, but opens a fresh ROOT trace (for background work with no
29
+ * inbound request: a scheduler tick, a server action). `seed` continues an
30
+ * inbound trace when its ids are provided.
31
+ */
32
+ export declare function withTrace<T>(name: string, fn: () => Promise<T> | T, opts?: SpanOpts & {
33
+ seed?: {
34
+ traceId?: string;
35
+ parentSpanId?: string;
36
+ };
37
+ }): Promise<T>;
38
+ export { bind } from "./context.js";
package/dist/api.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The ergonomic surface feature code reaches for: logEvent, span/withSpan,
3
+ * withTrace, plus the bind/current re-exports.
4
+ *
5
+ * Mirrors the agent's `obs/api.py`. The guiding rule is the same one that kept
6
+ * the agent framework small and is the explicit lesson from the abandoned OTel
7
+ * attempt: instrument the few meaningful seams and decisions, NOT every line. A
8
+ * handful of events/spans per flow, never hundreds. If you are tempted to add a
9
+ * fifth logEvent to one function, you probably want a span instead.
10
+ */
11
+ import { bind, runWithSpan, runWithTrace } from "./context.js";
12
+ import { otelEnabled, withOtelSpan } from "./otel.js";
13
+ import { emit, normalizeLevel } from "./record.js";
14
+ /** The default logger name for one-off events not tied to a module logger. */
15
+ const DEFAULT_LOGGER = "event";
16
+ /**
17
+ * Emit one named story beat.
18
+ *
19
+ * `event` is the machine-stable key (e.g. "lab.launch.started"); `message` is
20
+ * the human sentence; `fields` carry the salient facts (ids, counts, durations,
21
+ * the actual target). Use it at meaningful seams.
22
+ */
23
+ export function logEvent(event, message, fields, opts) {
24
+ emit(normalizeLevel(opts?.level ?? "INFO"), opts?.logger ?? DEFAULT_LOGGER, event, message ?? "", fields, opts?.error);
25
+ }
26
+ /**
27
+ * Open a child span around a unit of work: mints a child span id, binds
28
+ * `fields` as baggage (so they appear on every line inside), times the body,
29
+ * and logs `<name>.start` / `<name>.finish` (with dur_ms). On a throw it logs
30
+ * `<name>.error` with the stack and re-raises. The parent context is restored
31
+ * automatically when the body settles.
32
+ */
33
+ export async function withSpan(name, fn, opts) {
34
+ const logger = opts?.logger ?? DEFAULT_LOGGER;
35
+ const level = normalizeLevel(opts?.level ?? "INFO");
36
+ const inner = () => runWithSpan(async () => {
37
+ if (opts?.fields)
38
+ bind(opts.fields);
39
+ const start = Date.now();
40
+ emit(level, logger, `${name}.start`, name, opts?.fields);
41
+ try {
42
+ const out = await fn();
43
+ emit(level, logger, `${name}.finish`, name, { dur_ms: Date.now() - start });
44
+ return out;
45
+ }
46
+ catch (err) {
47
+ emit("ERROR", logger, `${name}.error`, `${name} failed: ${err instanceof Error ? err.message : String(err)}`, { dur_ms: Date.now() - start }, err);
48
+ throw err;
49
+ }
50
+ });
51
+ // Open a real OTel child span when export is on, so the domain story appears
52
+ // in the backend; the logs inside adopt the OTel span ids via context.current.
53
+ return otelEnabled() ? withOtelSpan(name, opts?.fields ?? {}, inner) : inner();
54
+ }
55
+ /**
56
+ * Like withSpan, but opens a fresh ROOT trace (for background work with no
57
+ * inbound request: a scheduler tick, a server action). `seed` continues an
58
+ * inbound trace when its ids are provided.
59
+ */
60
+ export async function withTrace(name, fn, opts) {
61
+ const logger = opts?.logger ?? DEFAULT_LOGGER;
62
+ const level = normalizeLevel(opts?.level ?? "INFO");
63
+ const inner = () => runWithTrace(opts?.seed ?? {}, async () => {
64
+ if (opts?.fields)
65
+ bind(opts.fields);
66
+ const start = Date.now();
67
+ emit(level, logger, `${name}.start`, name, opts?.fields);
68
+ try {
69
+ const out = await fn();
70
+ emit(level, logger, `${name}.finish`, name, { dur_ms: Date.now() - start });
71
+ return out;
72
+ }
73
+ catch (err) {
74
+ emit("ERROR", logger, `${name}.error`, `${name} failed: ${err instanceof Error ? err.message : String(err)}`, { dur_ms: Date.now() - start }, err);
75
+ throw err;
76
+ }
77
+ });
78
+ return otelEnabled() ? withOtelSpan(name, opts?.fields ?? {}, inner) : inner();
79
+ }
80
+ export { bind } from "./context.js";
@@ -0,0 +1,83 @@
1
+ /**
2
+ * What a service tells the library about itself, and the module-level home
3
+ * for it.
4
+ *
5
+ * Everything else in this package is generic. These are the only knobs, and
6
+ * they exist for one reason: four services already run this code with four
7
+ * env prefixes and four sets of live env files. `envPrefix` means none of
8
+ * those files change when a service adopts the package.
9
+ *
10
+ * `configure()` is called once, from the app's `instrumentation.ts`, before
11
+ * anything logs. It is idempotent by design rather than by accident: Next
12
+ * calls `register()` more than once across HMR and can isolate the
13
+ * instrumentation hook from route handlers, so a second call with the same
14
+ * service name is a no-op and a call with a DIFFERENT one is a mistake worth
15
+ * hearing about.
16
+ */
17
+ /** How a path becomes a metric label. See `routeTemplate` below. */
18
+ export type RouteTemplate = (path: string) => string;
19
+ /** Where a request's trace should continue from. See `traceSeed` below. */
20
+ export type TraceSeedFn = (req: Request) => {
21
+ traceId?: string;
22
+ parentSpanId?: string;
23
+ };
24
+ export type ObsConfig = {
25
+ /**
26
+ * The service's own name: the OTel tracer and meter name, the fallback for
27
+ * `service.name`, and the stem of the two log files. Use the unit name,
28
+ * e.g. "my-service".
29
+ */
30
+ service: string;
31
+ /**
32
+ * The prefix on this service's own env vars: "MYSVC" reads
33
+ * MYSVC_LOG_TEXT_FILE, MYSVC_VAR_DIR and the rest. The generic names
34
+ * (LOG_LEVEL, LOG_FORMAT, OTEL_*) are never prefixed. This is the whole
35
+ * compatibility story: a service adopting the library keeps the env file
36
+ * it already has, unchanged.
37
+ */
38
+ envPrefix: string;
39
+ /** State dir when <PREFIX>_VAR_DIR is unset. Default /var/lib/<service>. */
40
+ defaultVarDir?: string;
41
+ /**
42
+ * Stem for the two log files under <var dir>/logs, when the env does not
43
+ * name a path. Defaults to the last dash-separated word of `service`, so
44
+ * "acme-web-api" writes api.log and api.jsonl.
45
+ */
46
+ fileStem?: string;
47
+ /**
48
+ * Collapse dynamic path segments before a path becomes a metric label:
49
+ * `/api/labs/abc123` to `/api/labs/{identifier}`. Without it every id is its
50
+ * own time series, which is how a metrics backend falls over. The patterns
51
+ * are the service's own routes, so the service supplies them. Omitted means
52
+ * the raw path, which is fine for a service with few dynamic routes.
53
+ */
54
+ routeTemplate?: RouteTemplate;
55
+ /**
56
+ * Where an inbound request's trace comes from, when it is not a W3C
57
+ * `traceparent`. Some front ends pass a correlation UUID of their own as a
58
+ * header or `?trace=`; accepting it makes a user's journey one trace rather
59
+ * than two unconnected halves. Omitted means `traceparent` only.
60
+ */
61
+ traceSeed?: TraceSeedFn;
62
+ /**
63
+ * Request paths logged at debug rather than info. Health checks and
64
+ * internal polling otherwise bury the requests a person cares about.
65
+ * Matched by prefix.
66
+ */
67
+ quietPathPrefixes?: string[];
68
+ };
69
+ type Resolved = Required<Pick<ObsConfig, "service" | "envPrefix" | "defaultVarDir" | "fileStem">> & Pick<ObsConfig, "routeTemplate" | "traceSeed"> & {
70
+ quietPathPrefixes: string[];
71
+ };
72
+ export declare function configure(cfg: ObsConfig): void;
73
+ /**
74
+ * The live config. Falling back rather than throwing is deliberate: a log
75
+ * line emitted before `configure()` (an import-time warning, say) should
76
+ * still come out, named honestly, instead of taking the process down.
77
+ */
78
+ export declare function obsConfig(): Resolved;
79
+ /** Test seam: forget the configuration so a test can set a different one. */
80
+ export declare function resetConfigForTests(): void;
81
+ /** Read one of this service's own prefixed variables. */
82
+ export declare function envVar(name: string, env?: NodeJS.ProcessEnv): string | undefined;
83
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * What a service tells the library about itself, and the module-level home
3
+ * for it.
4
+ *
5
+ * Everything else in this package is generic. These are the only knobs, and
6
+ * they exist for one reason: four services already run this code with four
7
+ * env prefixes and four sets of live env files. `envPrefix` means none of
8
+ * those files change when a service adopts the package.
9
+ *
10
+ * `configure()` is called once, from the app's `instrumentation.ts`, before
11
+ * anything logs. It is idempotent by design rather than by accident: Next
12
+ * calls `register()` more than once across HMR and can isolate the
13
+ * instrumentation hook from route handlers, so a second call with the same
14
+ * service name is a no-op and a call with a DIFFERENT one is a mistake worth
15
+ * hearing about.
16
+ */
17
+ let current = null;
18
+ function resolve(cfg) {
19
+ const service = cfg.service.trim();
20
+ if (!service)
21
+ throw new Error("[obs] configure() needs a service name");
22
+ const prefix = cfg.envPrefix.trim().toUpperCase();
23
+ if (!/^[A-Z][A-Z0-9_]*$/.test(prefix)) {
24
+ throw new Error(`[obs] envPrefix '${cfg.envPrefix}' is not a usable env var prefix`);
25
+ }
26
+ return {
27
+ service,
28
+ envPrefix: prefix,
29
+ defaultVarDir: cfg.defaultVarDir ?? `/var/lib/${service}`,
30
+ fileStem: cfg.fileStem ?? (service.split("-").pop() || service),
31
+ routeTemplate: cfg.routeTemplate,
32
+ traceSeed: cfg.traceSeed,
33
+ quietPathPrefixes: cfg.quietPathPrefixes ?? [],
34
+ };
35
+ }
36
+ export function configure(cfg) {
37
+ const next = resolve(cfg);
38
+ if (current && current.service !== next.service) {
39
+ // Two services in one process is not a thing this library supports, and
40
+ // the symptom (logs filed under the wrong service) is miserable to chase.
41
+ throw new Error(`[obs] already configured as '${current.service}'; cannot reconfigure as '${next.service}'`);
42
+ }
43
+ current = next;
44
+ }
45
+ /**
46
+ * The live config. Falling back rather than throwing is deliberate: a log
47
+ * line emitted before `configure()` (an import-time warning, say) should
48
+ * still come out, named honestly, instead of taking the process down.
49
+ */
50
+ export function obsConfig() {
51
+ return current ?? (current = resolve({ service: "unconfigured", envPrefix: "OBS" }));
52
+ }
53
+ /** Test seam: forget the configuration so a test can set a different one. */
54
+ export function resetConfigForTests() {
55
+ current = null;
56
+ }
57
+ /** Read one of this service's own prefixed variables. */
58
+ export function envVar(name, env = process.env) {
59
+ return env[`${obsConfig().envPrefix}_${name}`];
60
+ }
@@ -0,0 +1,88 @@
1
+ import type { Fields } from "./types.js";
2
+ /** The ambient trace state for the current async scope. */
3
+ export type TraceState = {
4
+ /** 32 hex chars, or "" when there is no active trace. */
5
+ traceId: string;
6
+ /** 16 hex chars, or "". */
7
+ spanId: string;
8
+ /** 16 hex chars, or "" for a root span. */
9
+ parentSpanId: string;
10
+ /** W3C sampled flag; we always log locally, this only guides a future exporter. */
11
+ sampled: boolean;
12
+ /** Domain attributes stamped onto every line in scope (lab/session/agent ids). */
13
+ baggage: Fields;
14
+ };
15
+ /** Mint a fresh 128-bit trace id as 32 lowercase hex chars (W3C format). */
16
+ export declare function newTraceId(): string;
17
+ /** Mint a fresh 64-bit span id as 16 lowercase hex chars (W3C format). */
18
+ export declare function newSpanId(): string;
19
+ /** The current trace state. When OTLP export is live, the active OTel span is
20
+ * authoritative for the ids (so logs match the backend exactly and traceHeaders
21
+ * injects the OTel trace); baggage stays in our AsyncLocalStorage store.
22
+ * Outside any trace, the empty stand-in. */
23
+ export declare function current(): TraceState;
24
+ export declare function currentTraceId(): string;
25
+ export declare function currentSpanId(): string;
26
+ export declare function currentBaggage(): Fields;
27
+ export declare function isSampled(): boolean;
28
+ /**
29
+ * Run `fn` inside a brand-new ROOT trace.
30
+ *
31
+ * `seed.traceId` continues an inbound trace (e.g. a `traceparent` from the
32
+ * upstream caller); when absent a fresh id is minted. `seed.parentSpanId` is the
33
+ * caller's span id from the inbound header, so our first span points back at the
34
+ * remote caller and the tree spans both services.
35
+ *
36
+ * Unlike the agent's save/restore pattern, `als.run` restores the parent store
37
+ * automatically when `fn` returns or throws - the runtime does the "finally" for
38
+ * us.
39
+ */
40
+ export declare function runWithTrace<T>(seed: {
41
+ traceId?: string;
42
+ parentSpanId?: string;
43
+ sampled?: boolean;
44
+ }, fn: () => T): T;
45
+ /**
46
+ * Run `fn` inside a CHILD span of the current trace (the current span becomes
47
+ * the parent). If no trace is active we mint one defensively, so a stray span
48
+ * still produces a usable (if un-parented) trace rather than blank ids.
49
+ *
50
+ * The child copies the parent's baggage by value, so a child's `bind()` cannot
51
+ * leak back up into the parent - matching the agent's copy-on-write contract.
52
+ */
53
+ export declare function runWithSpan<T>(fn: () => T): T;
54
+ /**
55
+ * Stamp domain attributes onto the current scope so they appear on every later
56
+ * line. This deliberately MUTATES the live store's baggage (the one object
57
+ * AsyncLocalStorage hands back for the whole scope); it is a no-op outside a
58
+ * trace. Child scopes snapshot baggage at creation, so this never leaks across
59
+ * sibling requests.
60
+ */
61
+ export declare function bind(attrs: Fields): void;
62
+ /**
63
+ * Parse a W3C `traceparent` header into `{ traceId, parentSpanId }`.
64
+ *
65
+ * Format: `version-traceid-spanid-flags`, e.g.
66
+ * `00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01`. Returns null for
67
+ * anything malformed so the caller mints a fresh root rather than trusting junk;
68
+ * the all-zero ids are explicitly invalid per the spec. This is the exact mirror
69
+ * of the agent's `parse_traceparent`, so each side accepts the other's output.
70
+ */
71
+ export declare function parseTraceparent(header: string | null | undefined): {
72
+ traceId: string;
73
+ parentSpanId: string;
74
+ } | null;
75
+ /**
76
+ * Accept a plain UUID as a trace id.
77
+ *
78
+ * Some front ends pass a correlation UUID rather than a W3C
79
+ * `traceparent`, and a 32-hex UUID with the dashes stripped IS a valid trace
80
+ * id. Taking it means a user's journey is one trace from the front end through
81
+ * the launch to the lab, instead of two unconnected halves.
82
+ *
83
+ * Returns null for anything that is not 32 hex characters, and for the
84
+ * all-zero id, which W3C defines as "no trace".
85
+ */
86
+ export declare function traceIdFromCorrelationId(raw: string | null | undefined): string | null;
87
+ /** Serialise ids into a W3C `traceparent` string for an outbound header. */
88
+ export declare function formatTraceparent(traceId: string, spanId: string, sampled: boolean): string;