@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 +57 -12
- package/dist/api.d.ts +2 -0
- package/dist/api.js +5 -5
- package/dist/config.d.ts +13 -1
- package/dist/config.js +2 -0
- package/dist/http.js +7 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/otel-start.d.ts +14 -3
- package/dist/otel-start.js +66 -13
- package/dist/otel.d.ts +30 -10
- package/dist/otel.js +104 -45
- package/dist/record.d.ts +5 -0
- package/dist/record.js +9 -2
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +16 -3
- package/dist/sinks.d.ts +25 -3
- package/dist/sinks.js +34 -13
- package/dist/spans.d.ts +161 -0
- package/dist/spans.js +387 -0
- package/dist/types.js +1 -1
- package/package.json +11 -6
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
|
-
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
on
|
|
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
|
|
104
|
-
`*_FILE` value is a path, or `1` for the default path, or
|
|
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-
|
|
127
|
-
`@opentelemetry/exporter-logs-otlp-proto`,
|
|
128
|
-
optional peers, imported dynamically
|
|
129
|
-
|
|
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 {
|
|
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
|
-
//
|
|
52
|
-
//
|
|
53
|
-
return
|
|
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
|
|
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 {
|
|
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
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
return
|
|
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
|
|
153
|
-
|
|
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.
|
package/dist/otel-start.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* instrumentation.ts on the Node runtime. Idempotent; returns
|
|
4
|
-
* live. Never throws - on any failure we stay first-party
|
|
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
|
+
};
|
package/dist/otel-start.js
CHANGED
|
@@ -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
|
-
*
|
|
23
|
-
* instrumentation.ts on the Node runtime. Idempotent; returns
|
|
24
|
-
* live. Never throws - on any failure we stay first-party
|
|
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
|
-
|
|
31
|
-
return false;
|
|
41
|
+
const exportOn = !!process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
32
42
|
try {
|
|
33
|
-
const { registerOTel } = await import("@vercel/otel");
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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>;
|