@diveinto/obs 1.0.4 → 1.1.0

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/README.md CHANGED
@@ -35,12 +35,33 @@ of the four, and each of those behaviours is pinned by a test.
35
35
  writer on the same file, and the two then rename it out from under each
36
36
  other at the rotation boundary. Rotation also refuses to overwrite an
37
37
  existing rotated file, so a day of records cannot be lost to a rename.
38
- - **Trace context** on `AsyncLocalStorage`, continuing an inbound W3C
39
- `traceparent` and propagating it outbound, so one `trace_id` spans every
40
- hop.
41
- - **Optional OTLP export** to any OpenTelemetry collector. Off unless
42
- `OTEL_EXPORTER_OTLP_ENDPOINT` is set, and the first-party logs never depend
43
- on it.
38
+ A single file also has a **ceiling** (256 MB by default,
39
+ `<PREFIX>_LOG_JSON_MAX_MB` / `_TEXT_MAX_MB`, 0 to remove): daily rotation
40
+ bounds how many files there are, not how large one can get, and a service
41
+ that drops to DEBUG or loops on an error can write more in an afternoon
42
+ than the fortnight around it. Worst case per service is now a number an
43
+ operator can reason about rather than the free space on the disk.
44
+ - **Real spans, always** (v1.1.0). `startOtel` registers the tracer whether
45
+ or not anything is exported, so span ids come from it either way: a
46
+ request's SERVER span (named by its route template, continuing an inbound
47
+ W3C `traceparent`, and using the framework's own SERVER span when it has
48
+ opened one), `withSpan` and `withTrace` for units of work, and
49
+ `continueFrom` for work another process or a later moment does on a
50
+ request's behalf. Every record carries the ids of the span it was written
51
+ in, and `traceHeaders()` sends them on, so one `trace_id` spans every hop.
52
+ - **The span tree in the files.** Each span at a process boundary (server,
53
+ client, producer, consumer) and each span this service opens is written
54
+ to the files when it ends, as one record: `event: span.end` with
55
+ `span.name`, `span.kind`, `span.status`, `dur_ms` and its links. A tool
56
+ that reads only the files can draw the tree an OTLP backend shows.
57
+ - **Optional OTLP export** to any OpenTelemetry collector, when
58
+ `OTEL_EXPORTER_OTLP_ENDPOINT` is set; the first-party logs never depend on
59
+ it. What leaves the process passes an allowlist (no credentials, network
60
+ addresses, user agents, query strings, full URLs or personal identifiers;
61
+ more with `exportDenyKeys`; messages, and an error's message, through
62
+ `exportScrub`); the files keep everything. Export is seen, not assumed: an `obs.export` record every
63
+ minute counts what was sent and what failed, and a failure is noted in the
64
+ files at most once a minute, never over the exporter that is failing.
44
65
  - **Serverless-aware**: on Vercel or Lambda the file sinks switch themselves
45
66
  off rather than failing, because there is no persistent writable disk.
46
67
 
@@ -81,6 +102,23 @@ export const GET = (req: Request) =>
81
102
  the response. `withAction` does the same for a server action in a fresh root
82
103
  trace.
83
104
 
105
+ Work done later, or by another process, on a request's behalf: store the
106
+ request's context with it, and continue from it.
107
+
108
+ ```ts
109
+ import { continueFrom, currentTraceparent } from "@diveinto/obs";
110
+
111
+ // in the request: keep its context with the job
112
+ await db.insert(jobs).values({ id, traceparent: currentTraceparent() });
113
+
114
+ // in the worker: the job's span is a child of the request's
115
+ await continueFrom(job.traceparent, "process jobs", () => run(job), { kind: "consumer" });
116
+ ```
117
+
118
+ A stored or forwarded context is untrusted input: one that is missing or
119
+ malformed starts a new trace and never fails the work. `links` ties a span to
120
+ the other requests it serves (a batch) without making it their child.
121
+
84
122
  ## Configuration
85
123
 
86
124
  | field | required | what it does |
@@ -92,6 +130,8 @@ trace.
92
130
  | `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 |
93
131
  | `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 |
94
132
  | `quietPathPrefixes` | no | Request paths whose completion logs at debug rather than info. Health checks and internal polling otherwise bury what matters |
133
+ | `exportDenyKeys` | no | More attribute and field keys that never leave the process over OTLP, beside the built-in ones. The files keep them |
134
+ | `exportScrub` | no | Rewrites a record's message before it is exported, for a service whose messages can name a person. The files keep the message as written |
95
135
 
96
136
  ### Environment
97
137
 
@@ -100,8 +140,10 @@ Generic, never prefixed: `LOG_LEVEL` (`trace`/`debug`/`info`/`warn`/`error`/
100
140
  `OTEL_*` set.
101
141
 
102
142
  Per service, prefixed: `<PREFIX>_VAR_DIR`, `<PREFIX>_LOG_TEXT_FILE`,
103
- `_TEXT_ROTATION`, `_TEXT_RETENTION`, and the same three for `_JSON_`. A
104
- `*_FILE` value is a path, or `1` for the default path, or `0`/`off`.
143
+ `_TEXT_ROTATION`, `_TEXT_RETENTION`, `_TEXT_MAX_MB`, and the same four for
144
+ `_JSON_`. A `*_FILE` value is a path, or `1` for the default path, or
145
+ `0`/`off`; `*_MAX_MB` is the per-file ceiling, 256 by default and 0 for
146
+ none.
105
147
 
106
148
  ## Cardinality
107
149
 
@@ -123,10 +165,13 @@ from the Node branch of `instrumentation.ts` and from route handlers, not
123
165
  from Edge middleware.
124
166
 
125
167
  `@opentelemetry/api` and `@opentelemetry/api-logs` are required peers. The
126
- SDK packages (`@vercel/otel`, `@opentelemetry/sdk-logs`,
127
- `@opentelemetry/exporter-logs-otlp-proto`, `@opentelemetry/resources`) are
128
- optional peers, imported dynamically and needed only if you use OTLP export
129
- via `@diveinto/obs/start`.
168
+ SDK packages (`@vercel/otel`, `@opentelemetry/sdk-trace-base`,
169
+ `@opentelemetry/sdk-logs`, `@opentelemetry/exporter-logs-otlp-proto`,
170
+ `@opentelemetry/resources`) are optional peers, imported dynamically by
171
+ `@diveinto/obs/start`: install them to have real spans (`@vercel/otel`, and
172
+ `@opentelemetry/sdk-trace-base` for export) and OTLP logs. Without them the
173
+ library falls back to ids of its own on `AsyncLocalStorage`, as before
174
+ v1.1.0.
130
175
 
131
176
  ## License
132
177
 
package/dist/api.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { SpanKindName } from "./spans.js";
1
2
  import type { Fields, LevelInput } from "./types.js";
2
3
  /**
3
4
  * Emit one named story beat.
@@ -15,6 +16,7 @@ type SpanOpts = {
15
16
  fields?: Fields;
16
17
  logger?: string;
17
18
  level?: LevelInput;
19
+ kind?: SpanKindName;
18
20
  };
19
21
  /**
20
22
  * Open a child span around a unit of work: mints a child span id, binds
package/dist/api.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * fifth logEvent to one function, you probably want a span instead.
10
10
  */
11
11
  import { bind, runWithSpan, runWithTrace } from "./context.js";
12
- import { otelEnabled, withOtelSpan } from "./otel.js";
12
+ import { withOtelRootSpan, withOtelSpan } from "./otel.js";
13
13
  import { emit, normalizeLevel } from "./record.js";
14
14
  /** The default logger name for one-off events not tied to a module logger. */
15
15
  const DEFAULT_LOGGER = "event";
@@ -48,9 +48,9 @@ export async function withSpan(name, fn, opts) {
48
48
  throw err;
49
49
  }
50
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();
51
+ // A real span, exported or not: the records inside adopt its ids via context.current, and its
52
+ // own record lands in the files when it ends.
53
+ return withOtelSpan(name, opts?.fields ?? {}, inner, opts?.kind);
54
54
  }
55
55
  /**
56
56
  * Like withSpan, but opens a fresh ROOT trace (for background work with no
@@ -75,6 +75,6 @@ export async function withTrace(name, fn, opts) {
75
75
  throw err;
76
76
  }
77
77
  });
78
- return otelEnabled() ? withOtelSpan(name, opts?.fields ?? {}, inner) : inner();
78
+ return withOtelRootSpan(name, opts?.fields ?? {}, inner, opts?.seed, opts?.kind);
79
79
  }
80
80
  export { bind } from "./context.js";
package/dist/config.d.ts CHANGED
@@ -65,9 +65,21 @@ export type ObsConfig = {
65
65
  * Matched by prefix.
66
66
  */
67
67
  quietPathPrefixes?: string[];
68
+ /**
69
+ * More attribute and field keys that never leave the process over OTLP, beside the built-in
70
+ * ones (credentials, network addresses, user agents, query strings and personal identifiers;
71
+ * see `exportable` in spans.ts). The files keep everything: they stay on the machine.
72
+ */
73
+ exportDenyKeys?: string[];
74
+ /**
75
+ * Rewrites a record's message before it is exported over OTLP, for a service whose messages
76
+ * can name a person (an id, an address). The files keep the message as written.
77
+ */
78
+ exportScrub?: (msg: string) => string;
68
79
  };
69
- type Resolved = Required<Pick<ObsConfig, "service" | "envPrefix" | "defaultVarDir" | "fileStem">> & Pick<ObsConfig, "routeTemplate" | "traceSeed"> & {
80
+ type Resolved = Required<Pick<ObsConfig, "service" | "envPrefix" | "defaultVarDir" | "fileStem">> & Pick<ObsConfig, "routeTemplate" | "traceSeed" | "exportScrub"> & {
70
81
  quietPathPrefixes: string[];
82
+ exportDenyKeys: string[];
71
83
  };
72
84
  export declare function configure(cfg: ObsConfig): void;
73
85
  /**
package/dist/config.js CHANGED
@@ -50,6 +50,8 @@ function resolve(cfg) {
50
50
  routeTemplate: cfg.routeTemplate,
51
51
  traceSeed: cfg.traceSeed,
52
52
  quietPathPrefixes: cfg.quietPathPrefixes ?? [],
53
+ exportDenyKeys: cfg.exportDenyKeys ?? [],
54
+ exportScrub: cfg.exportScrub,
53
55
  };
54
56
  }
55
57
  export function configure(cfg) {
package/dist/http.js CHANGED
@@ -17,7 +17,7 @@ import { bind, currentSpanId, currentTraceId, formatTraceparent, isSampled, pars
17
17
  import { logEvent } from "./api.js";
18
18
  import { obsConfig } from "./config.js";
19
19
  import { metricCounter, metricHistogram } from "./metrics.js";
20
- import { otelEnabled, withOtelServerSpan, withOtelSpan } from "./otel.js";
20
+ import { withOtelRootSpan, withOtelServerSpan } from "./otel.js";
21
21
  /**
22
22
  * The headers to attach to an outbound request to the agent so it CONTINUES
23
23
  * this trace. Returns `{ traceparent }` when a trace is active, else `{}`. The
@@ -111,10 +111,10 @@ export async function withApiTrace(req, handler, opts) {
111
111
  throw err;
112
112
  }
113
113
  });
114
- // When OTLP export is on, open a real SERVER span that CONTINUES the inbound
115
- // W3C traceparent, then run the logging body inside it (which now adopts the
116
- // OTel ids). When off, just run the body on the AsyncLocalStorage path.
117
- return otelEnabled() ? withOtelServerSpan(`${method} ${path}`, req.headers, fields, inner) : inner();
114
+ // The request's SERVER span, continuing an inbound W3C traceparent, exported or not; the
115
+ // logging body runs inside it and adopts its ids. Named by the route's template, never the raw
116
+ // path: a span name is a grouping key, and an id in it makes every request its own group.
117
+ return withOtelServerSpan(`${method} ${routeLabel(path)}`, req.headers, { ...fields, "http.route": routeLabel(path) }, inner);
118
118
  }
119
119
  /**
120
120
  * Wrap a server action (which has no NextRequest) in a FRESH root trace named
@@ -149,9 +149,8 @@ export async function withAction(name, fn, fields) {
149
149
  throw err;
150
150
  }
151
151
  });
152
- // A server action has no inbound request, so when export is on this opens a
153
- // fresh root OTel span (startActiveSpan with no active parent).
154
- return otelEnabled() ? withOtelSpan(`action ${name}`, { "action.name": name, ...(fields ?? {}) }, inner) : inner();
152
+ // A server action has no inbound request: a fresh root span of its own.
153
+ return withOtelRootSpan(`action ${name}`, { "action.name": name, ...(fields ?? {}) }, inner);
155
154
  }
156
155
  /**
157
156
  * Record the action duration histogram + invocation counter with the SAME
package/dist/index.d.ts CHANGED
@@ -53,7 +53,8 @@ export { metricCounter, metricHistogram, timed } from "./metrics.js";
53
53
  export { renderJson, renderPretty } from "./render.js";
54
54
  export { configureSinks, makeFileSink, makeStdoutSink } from "./sinks.js";
55
55
  export { isServerless, resetLoggingForTests, resolveFileTargets, setupLogging, type FileTarget, } from "./setup.js";
56
- export { currentIds, makeOtelLogSink, otelEnabled, withOtelServerSpan, withOtelSpan } from "./otel.js";
56
+ export { currentIds, makeOtelLogSink, otelEnabled, withOtelRootSpan, withOtelServerSpan, withOtelSpan, } from "./otel.js";
57
+ export { continueFrom, currentTraceparent, exportable, exportCounters, spanContextFromTraceparent, traceparentOf, type ExportCounters, type SpanKindName, } from "./spans.js";
57
58
  export { resolveTraceSeed, traceHeaders, withAction, withApiTrace } from "./http.js";
58
59
  /**
59
60
  * Configure the library and hand back the surface an app actually uses.
package/dist/index.js CHANGED
@@ -52,7 +52,8 @@ export { metricCounter, metricHistogram, timed } from "./metrics.js";
52
52
  export { renderJson, renderPretty } from "./render.js";
53
53
  export { configureSinks, makeFileSink, makeStdoutSink } from "./sinks.js";
54
54
  export { isServerless, resetLoggingForTests, resolveFileTargets, setupLogging, } from "./setup.js";
55
- export { currentIds, makeOtelLogSink, otelEnabled, withOtelServerSpan, withOtelSpan } from "./otel.js";
55
+ export { currentIds, makeOtelLogSink, otelEnabled, withOtelRootSpan, withOtelServerSpan, withOtelSpan, } from "./otel.js";
56
+ export { continueFrom, currentTraceparent, exportable, exportCounters, spanContextFromTraceparent, traceparentOf, } from "./spans.js";
56
57
  export { resolveTraceSeed, traceHeaders, withAction, withApiTrace } from "./http.js";
57
58
  /**
58
59
  * Configure the library and hand back the surface an app actually uses.
@@ -1,6 +1,17 @@
1
1
  /**
2
- * Start OTLP export iff an endpoint is configured. Called once from
3
- * instrumentation.ts on the Node runtime. Idempotent; returns whether export is
4
- * live. Never throws - on any failure we stay first-party only.
2
+ * Register the tracer, always, and OTLP export when an endpoint is configured.
3
+ * Called once from instrumentation.ts on the Node runtime. Idempotent; returns
4
+ * whether export is live. Never throws - on any failure we stay first-party
5
+ * only.
5
6
  */
6
7
  export declare function startOtel(serviceName: string): Promise<boolean>;
8
+ /**
9
+ * Where a signal is exported, from the standard environment: the signal's own
10
+ * endpoint (`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, used as given), else the
11
+ * shared one with `/v1/<signal>` appended; and the headers, shared then the
12
+ * signal's own, each `key=value` comma separated and percent-decoded.
13
+ */
14
+ export declare function otlpTarget(signal: "traces" | "logs", env?: NodeJS.ProcessEnv): {
15
+ url: string;
16
+ headers: Record<string, string>;
17
+ };
@@ -9,46 +9,98 @@
9
9
  *
10
10
  * Published as the `@diveinto/obs/start` subpath for the same reason.
11
11
  *
12
+ * The tracer is registered whether or not an endpoint is configured: span ids
13
+ * come from it either way, and each span's record goes to the files when it
14
+ * ends (spans.ts), so the files hold the same tree with export on and off.
15
+ * Only the exporters depend on the endpoint.
16
+ *
12
17
  * Never throws: on any failure the service stays first-party only, still
13
18
  * writing stdout and its two files. Losing export is not worth losing logs.
14
19
  */
15
20
  import { logs } from "@opentelemetry/api-logs";
21
+ import { FileSpanProcessor, GuardedLogExporter, GuardedSpanExporter, startExportHeartbeat, } from "./spans.js";
16
22
  // Module-local to the start path: `otelEnabled()` in otel.ts reads the env
17
23
  // instead, because Next can run instrumentation.ts in a different module
18
24
  // context from the route handlers, where a flag set here would read false.
19
25
  let enabled = false;
20
26
  let started = false;
27
+ // Batching for both exporters: a bounded queue that drops when full rather than
28
+ // blocking the work that made the span, and an export that gives up rather than
29
+ // holding a request or a shutdown behind a slow backend.
30
+ const BATCH = { maxQueueSize: 2048, maxExportBatchSize: 512, scheduledDelayMillis: 2000, exportTimeoutMillis: 10_000 };
21
31
  /**
22
- * Start OTLP export iff an endpoint is configured. Called once from
23
- * instrumentation.ts on the Node runtime. Idempotent; returns whether export is
24
- * live. Never throws - on any failure we stay first-party only.
32
+ * Register the tracer, always, and OTLP export when an endpoint is configured.
33
+ * Called once from instrumentation.ts on the Node runtime. Idempotent; returns
34
+ * whether export is live. Never throws - on any failure we stay first-party
35
+ * only.
25
36
  */
26
37
  export async function startOtel(serviceName) {
27
38
  if (started)
28
39
  return enabled;
29
40
  started = true;
30
- if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT)
31
- return false;
41
+ const exportOn = !!process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
32
42
  try {
33
- const { registerOTel } = await import("@vercel/otel");
34
- // registerOTel reads OTEL_EXPORTER_OTLP_* (endpoint/headers/protocol) and
35
- // OTEL_RESOURCE_ATTRIBUTES from the env. serviceName is the fallback label.
43
+ const { registerOTel, OTLPHttpProtoTraceExporter } = await import("@vercel/otel");
44
+ const processors = [new FileSpanProcessor()];
45
+ if (exportOn) {
46
+ const { BatchSpanProcessor } = await import("@opentelemetry/sdk-trace-base");
47
+ const exporter = new GuardedSpanExporter(new OTLPHttpProtoTraceExporter(otlpTarget("traces")));
48
+ processors.push(new BatchSpanProcessor(exporter, BATCH));
49
+ }
36
50
  // TRACES + LOGS only, on purpose: no metricReader is passed here, so no
37
51
  // metrics leave the process regardless of env vars. This is a scar, not a
38
52
  // preference: auto-exporting metrics alongside traces once produced tens
39
53
  // of millions of samples a month that nobody read. Do not add a
40
54
  // metricReader without a named consumer, an export allowlist, and a
41
55
  // cardinality cap on every attribute.
42
- registerOTel({ serviceName });
43
- await startLogExport(serviceName);
44
- enabled = true;
45
- return true;
56
+ //
57
+ // Every trace is sampled: an inbound "not sampled" flag from outside must
58
+ // not turn tracing off for the work it reaches.
59
+ registerOTel({
60
+ serviceName: process.env.OTEL_SERVICE_NAME || serviceName,
61
+ traceSampler: "always_on",
62
+ spanProcessors: processors,
63
+ traceExporter: undefined,
64
+ });
65
+ if (exportOn)
66
+ await startLogExport(serviceName);
67
+ startExportHeartbeat(exportOn);
68
+ enabled = exportOn;
69
+ return exportOn;
46
70
  }
47
71
  catch (e) {
48
72
  process.stderr.write(`[obs] OTel init failed (${String(e)}); staying first-party only\n`);
49
73
  return false;
50
74
  }
51
75
  }
76
+ /**
77
+ * Where a signal is exported, from the standard environment: the signal's own
78
+ * endpoint (`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, used as given), else the
79
+ * shared one with `/v1/<signal>` appended; and the headers, shared then the
80
+ * signal's own, each `key=value` comma separated and percent-decoded.
81
+ */
82
+ export function otlpTarget(signal, env = process.env) {
83
+ const own = env[`OTEL_EXPORTER_OTLP_${signal.toUpperCase()}_ENDPOINT`];
84
+ const shared = (env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "").replace(/\/+$/, "");
85
+ const url = own || `${shared}/v1/${signal}`;
86
+ const headers = {};
87
+ for (const raw of [env.OTEL_EXPORTER_OTLP_HEADERS, env[`OTEL_EXPORTER_OTLP_${signal.toUpperCase()}_HEADERS`]]) {
88
+ for (const pair of (raw ?? "").split(",")) {
89
+ const eq = pair.indexOf("=");
90
+ if (eq <= 0)
91
+ continue;
92
+ const k = pair.slice(0, eq).trim();
93
+ const v = pair.slice(eq + 1).trim();
94
+ try {
95
+ headers[k] = decodeURIComponent(v);
96
+ }
97
+ catch {
98
+ headers[k] = v;
99
+ }
100
+ }
101
+ }
102
+ return { url, headers };
103
+ }
52
104
  /**
53
105
  * Set up the OTLP LOGS signal (separate from traces, which @vercel/otel owns).
54
106
  * Installs a global LoggerProvider + OTLP/protobuf log exporter, so the log sink
@@ -67,9 +119,10 @@ async function startLogExport(serviceName) {
67
119
  // also pull service.version etc. from OTEL_RESOURCE_ATTRIBUTES so the LOGS
68
120
  // resource matches the TRACES resource registerOTel builds.
69
121
  const resource = defaultResource().merge(resourceFromAttributes(logResourceAttrs(serviceName)));
122
+ const exporter = new GuardedLogExporter(new OTLPLogExporter());
70
123
  const provider = new LoggerProvider({
71
124
  resource,
72
- processors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
125
+ processors: [new BatchLogRecordProcessor(exporter, BATCH)],
73
126
  });
74
127
  logs.setGlobalLoggerProvider(provider);
75
128
  }
package/dist/otel.d.ts CHANGED
@@ -1,10 +1,21 @@
1
+ import { type SpanKindName } from "./spans.js";
1
2
  import { type Fields, type Sink } from "./types.js";
3
+ /**
4
+ * Whether OTLP export is configured: what the log sink and the metrics read. Spans never ask; the
5
+ * tracer runs either way. Env-based (process-global) on purpose: Next can run instrumentation.ts in
6
+ * a different module context than the route handlers, so a per-module flag set by startOtel would
7
+ * read false in handlers.
8
+ */
2
9
  export declare function otelEnabled(): boolean;
3
10
  /**
4
- * A Sink that ships each obs record to the OTLP backend as a log, correlated to
5
- * the active span (emit() reads the current context, and our records are emitted
6
- * inside the request/domain span). Returns null when export is off. The internal
7
- * export fetch does not pass back through our sinks, so there is no feedback loop.
11
+ * A Sink that ships each obs record to the OTLP backend as a log carrying the record's own trace
12
+ * and span ids, not only an active span's: a record written for a span after it ended, or in work
13
+ * continued from a stored traceparent, still sits in its trace. Returns null when export is off.
14
+ *
15
+ * Two kinds of record stay in the files: a span's own record (the span itself is exported) and one
16
+ * marked `obs.local` (an export failure, which would otherwise travel over the failing exporter).
17
+ * What does leave passes `exportable`, and the message passes the service's `exportScrub`. The
18
+ * internal export fetch does not pass back through our sinks, so there is no feedback loop.
8
19
  */
9
20
  export declare function makeOtelLogSink(): Sink | null;
10
21
  /** The active OTel span's ids as W3C hex, or null when there is no valid span.
@@ -16,11 +27,20 @@ export declare function currentIds(): {
16
27
  parentSpanId: string;
17
28
  } | null;
18
29
  /**
19
- * Open a SERVER span continuing the inbound W3C traceparent (extracted from the
20
- * request headers), so this service's span nests under the caller's trace. A
21
- * no-op passthrough when export is off.
30
+ * The request's SERVER span. When the framework has already opened one for this request (Next.js
31
+ * does, continuing an inbound traceparent itself), that span is named `name` and given `attrs`,
32
+ * and `fn` runs in it: one SERVER span per request, never two siblings. Otherwise a SERVER span is
33
+ * opened here, continuing the inbound W3C traceparent extracted from the headers.
22
34
  */
23
35
  export declare function withOtelServerSpan<T>(name: string, headers: Headers, attrs: Fields, fn: () => Promise<T>): Promise<T>;
24
- /** Open a child span around a unit of work (domain story / outbound). A no-op
25
- * passthrough when export is off. */
26
- export declare function withOtelSpan<T>(name: string, attrs: Fields, fn: () => Promise<T>): Promise<T>;
36
+ /** Open a child span around a unit of work: internal by default, `kind` for a call out or a message. */
37
+ export declare function withOtelSpan<T>(name: string, attrs: Fields, fn: () => Promise<T>, kind?: SpanKindName): Promise<T>;
38
+ /**
39
+ * Open a span that starts a trace of its own, whatever span is in force: background work, a server
40
+ * action. `seed` continues a trace whose ids arrived another way (a stored or forwarded context):
41
+ * its trace id and, as the parent, its span id.
42
+ */
43
+ export declare function withOtelRootSpan<T>(name: string, attrs: Fields, fn: () => Promise<T>, seed?: {
44
+ traceId?: string;
45
+ parentSpanId?: string;
46
+ }, kind?: SpanKindName): Promise<T>;