@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/dist/spans.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spans in the files, export that can be seen, and context for work that outlives its request.
|
|
3
|
+
*
|
|
4
|
+
* Three rules shape this module:
|
|
5
|
+
*
|
|
6
|
+
* - Every span the service cares about is written to its own files when it ends, as one
|
|
7
|
+
* record (`event: span.end`) carrying the span's own ids, name, kind, status and duration.
|
|
8
|
+
* A reader that has only the files (a log tool on the machine, an operator over ssh) can then
|
|
9
|
+
* draw the same span tree an OTLP backend shows; without it, a span that logged nothing is
|
|
10
|
+
* invisible, and every child of it reads as an orphan.
|
|
11
|
+
* - Nothing personal or secret leaves the process. What is exported passes `exportable`; the
|
|
12
|
+
* files keep everything, because they stay on the machine.
|
|
13
|
+
* - Export is seen, not assumed. Counters track what was sent and what failed, a periodic
|
|
14
|
+
* `obs.export` record says so, and a failure is logged to the files at most once a minute,
|
|
15
|
+
* never over the exporter that is failing.
|
|
16
|
+
*
|
|
17
|
+
* The span tree's ids come from the tracer whether or not anything is exported (see
|
|
18
|
+
* otel-start.ts), so the files look the same with export on and off.
|
|
19
|
+
*/
|
|
20
|
+
import { context as otelContext, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace, TraceFlags, } from "@opentelemetry/api";
|
|
21
|
+
import { obsConfig } from "./config.js";
|
|
22
|
+
import { newSpanId, newTraceId, runWithTrace } from "./context.js";
|
|
23
|
+
import { emitWith } from "./record.js";
|
|
24
|
+
/** The instrumentation scope of the spans this library opens itself. */
|
|
25
|
+
export const LIBRARY_SCOPE = "@diveinto/obs";
|
|
26
|
+
// ---- what may leave the process ---------------------------------------------------------------
|
|
27
|
+
// Keys whose values identify a person, a machine on a network, or a credential, or carry a query
|
|
28
|
+
// string or a full URL (which can hold any of those). Exact OpenTelemetry semantic-convention keys
|
|
29
|
+
// first, then a rule for the rest by their last word.
|
|
30
|
+
const DENY_EXACT = new Set([
|
|
31
|
+
"url.full",
|
|
32
|
+
"url.query",
|
|
33
|
+
"url.path",
|
|
34
|
+
"http.url",
|
|
35
|
+
"http.target",
|
|
36
|
+
"http.path",
|
|
37
|
+
"client.address",
|
|
38
|
+
"client.port",
|
|
39
|
+
"network.peer.address",
|
|
40
|
+
"net.peer.ip",
|
|
41
|
+
"net.sock.peer.addr",
|
|
42
|
+
"http.client_ip",
|
|
43
|
+
"user_agent.original",
|
|
44
|
+
"http.user_agent",
|
|
45
|
+
"enduser.id",
|
|
46
|
+
"user.id",
|
|
47
|
+
"user.email",
|
|
48
|
+
"user.name",
|
|
49
|
+
]);
|
|
50
|
+
// A user agent in any spelling (`user_agent`, `user-agent`, `useragent`, `http.user.agent`).
|
|
51
|
+
const DENY_LAST_WORD = /(^|[._-])(token|password|passwd|secret|authorization|cookie|kubeconfig|email|sub|subject|ip|referer|referrer|user[._-]?agent|query)$/i;
|
|
52
|
+
/**
|
|
53
|
+
* Whether an attribute or field may be exported: not a credential, an address, a user agent, a
|
|
54
|
+
* query, a full URL or a personal identifier, and not one the service named in `exportDenyKeys`.
|
|
55
|
+
*/
|
|
56
|
+
export function exportable(key) {
|
|
57
|
+
if (DENY_EXACT.has(key) || DENY_LAST_WORD.test(key))
|
|
58
|
+
return false;
|
|
59
|
+
return !obsConfig().exportDenyKeys.includes(key);
|
|
60
|
+
}
|
|
61
|
+
function exportableAttributes(attrs) {
|
|
62
|
+
const out = {};
|
|
63
|
+
if (!attrs)
|
|
64
|
+
return out;
|
|
65
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
66
|
+
if (exportable(k))
|
|
67
|
+
out[k] = v;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The span as it may leave the process: the same span with every attribute `exportable` refuses
|
|
73
|
+
* removed, on the span, its events and its links. A plain object rather than a wrapper around the
|
|
74
|
+
* SDK's span, so nothing in the exporter reaches back into the original's private state.
|
|
75
|
+
*/
|
|
76
|
+
function forExport(span) {
|
|
77
|
+
const sc = span.spanContext();
|
|
78
|
+
return {
|
|
79
|
+
name: span.name,
|
|
80
|
+
kind: span.kind,
|
|
81
|
+
spanContext: () => sc,
|
|
82
|
+
parentSpanContext: span.parentSpanContext,
|
|
83
|
+
parentSpanId: span.parentSpanId,
|
|
84
|
+
startTime: span.startTime,
|
|
85
|
+
endTime: span.endTime,
|
|
86
|
+
duration: span.duration,
|
|
87
|
+
status: span.status,
|
|
88
|
+
attributes: exportableAttributes(span.attributes),
|
|
89
|
+
links: span.links.map((l) => ({ ...l, attributes: exportableAttributes(l.attributes) })),
|
|
90
|
+
events: span.events.map((e) => ({ ...e, attributes: exportableAttributes(e.attributes) })),
|
|
91
|
+
resource: span.resource,
|
|
92
|
+
instrumentationScope: span.instrumentationScope,
|
|
93
|
+
instrumentationLibrary: span.instrumentationLibrary,
|
|
94
|
+
ended: span.ended,
|
|
95
|
+
droppedAttributesCount: span.droppedAttributesCount,
|
|
96
|
+
droppedEventsCount: span.droppedEventsCount,
|
|
97
|
+
droppedLinksCount: span.droppedLinksCount,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const counters = {
|
|
101
|
+
spansWritten: 0,
|
|
102
|
+
spansExported: 0,
|
|
103
|
+
spansFailed: 0,
|
|
104
|
+
logsExported: 0,
|
|
105
|
+
logsFailed: 0,
|
|
106
|
+
};
|
|
107
|
+
/** The counters as they stand, a copy. */
|
|
108
|
+
export function exportCounters() {
|
|
109
|
+
return { ...counters };
|
|
110
|
+
}
|
|
111
|
+
// The process's own trace, for the records this module writes outside any request: the heartbeat
|
|
112
|
+
// and an export failure. A record with no trace id at all reads as a component that lost its
|
|
113
|
+
// tracing, so they carry this one. Minted on first use, never at import: this module sits in an
|
|
114
|
+
// import cycle (records, context, the tracer), and must do nothing while it loads.
|
|
115
|
+
let processTrace;
|
|
116
|
+
function processIds() {
|
|
117
|
+
processTrace ??= { traceId: newTraceId(), spanId: newSpanId(), parentSpanId: "" };
|
|
118
|
+
return processTrace;
|
|
119
|
+
}
|
|
120
|
+
const FAILURE_EVERY_MS = 60_000;
|
|
121
|
+
let lastFailureAt = 0;
|
|
122
|
+
/**
|
|
123
|
+
* Note an export failure in the files, at most once a minute, and never over OTLP: a failure
|
|
124
|
+
* logged through the exporter that failed would only make another.
|
|
125
|
+
*/
|
|
126
|
+
function noteFailure(signal, error) {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
if (now - lastFailureAt < FAILURE_EVERY_MS)
|
|
129
|
+
return;
|
|
130
|
+
lastFailureAt = now;
|
|
131
|
+
emitWith(processIds(), {}, "WARNING", "obs", "obs.export_failed", `exporting ${signal} failed; the files are complete and export carries on trying`, { "obs.local": true, signal, ...counters }, error);
|
|
132
|
+
}
|
|
133
|
+
let heartbeat;
|
|
134
|
+
/**
|
|
135
|
+
* Write an `obs.export` record now and then every `everyMs`: whether export is on, and the
|
|
136
|
+
* counters. A component configured to export that has exported nothing in an hour shows it here.
|
|
137
|
+
* The timer never holds the process open.
|
|
138
|
+
*/
|
|
139
|
+
export function startExportHeartbeat(exportOn, everyMs = 60_000) {
|
|
140
|
+
const beat = () => emitWith(processIds(), {}, "INFO", "obs", "obs.export", exportOn
|
|
141
|
+
? `export on: ${counters.spansExported} span(s) and ${counters.logsExported} log(s) sent, ${counters.spansFailed + counters.logsFailed} failed`
|
|
142
|
+
: `export off: ${counters.spansWritten} span(s) written to the files only`, { "obs.export": exportOn ? "on" : "off", ...counters });
|
|
143
|
+
if (heartbeat)
|
|
144
|
+
clearInterval(heartbeat);
|
|
145
|
+
beat();
|
|
146
|
+
if (!exportOn)
|
|
147
|
+
return;
|
|
148
|
+
heartbeat = setInterval(beat, everyMs);
|
|
149
|
+
heartbeat.unref?.();
|
|
150
|
+
}
|
|
151
|
+
/** Test seam: stop the heartbeat and zero the counters. */
|
|
152
|
+
export function resetExportForTests() {
|
|
153
|
+
if (heartbeat)
|
|
154
|
+
clearInterval(heartbeat);
|
|
155
|
+
heartbeat = undefined;
|
|
156
|
+
lastFailureAt = 0;
|
|
157
|
+
for (const k of Object.keys(counters))
|
|
158
|
+
counters[k] = 0;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A span exporter that sends only what may leave the process (`exportable`), counts what it sent
|
|
162
|
+
* and what failed, and notes a failure in the files. Never throws into the SDK.
|
|
163
|
+
*/
|
|
164
|
+
export class GuardedSpanExporter {
|
|
165
|
+
inner;
|
|
166
|
+
constructor(inner) {
|
|
167
|
+
this.inner = inner;
|
|
168
|
+
}
|
|
169
|
+
export(spans, done) {
|
|
170
|
+
const settle = (r) => {
|
|
171
|
+
if (r.code === 0)
|
|
172
|
+
counters.spansExported += spans.length;
|
|
173
|
+
else {
|
|
174
|
+
counters.spansFailed += spans.length;
|
|
175
|
+
noteFailure("spans", r.error);
|
|
176
|
+
}
|
|
177
|
+
done(r);
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
this.inner.export(spans.map(forExport), settle);
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
settle({ code: 1, error: e });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
shutdown() {
|
|
187
|
+
return this.inner.shutdown();
|
|
188
|
+
}
|
|
189
|
+
forceFlush() {
|
|
190
|
+
return this.inner.forceFlush?.() ?? Promise.resolve();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** A log exporter that counts what it sent and what failed, as GuardedSpanExporter does. */
|
|
194
|
+
export class GuardedLogExporter {
|
|
195
|
+
inner;
|
|
196
|
+
constructor(inner) {
|
|
197
|
+
this.inner = inner;
|
|
198
|
+
}
|
|
199
|
+
export(records, done) {
|
|
200
|
+
const settle = (r) => {
|
|
201
|
+
if (r.code === 0)
|
|
202
|
+
counters.logsExported += records.length;
|
|
203
|
+
else {
|
|
204
|
+
counters.logsFailed += records.length;
|
|
205
|
+
noteFailure("logs", r.error);
|
|
206
|
+
}
|
|
207
|
+
done(r);
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
this.inner.export(records, settle);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
settle({ code: 1, error: e });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
shutdown() {
|
|
217
|
+
return this.inner.shutdown();
|
|
218
|
+
}
|
|
219
|
+
forceFlush() {
|
|
220
|
+
return this.inner.forceFlush?.() ?? Promise.resolve();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ---- spans into the files ---------------------------------------------------------------------
|
|
224
|
+
const KIND_NAMES = {
|
|
225
|
+
[SpanKind.INTERNAL]: "internal",
|
|
226
|
+
[SpanKind.SERVER]: "server",
|
|
227
|
+
[SpanKind.CLIENT]: "client",
|
|
228
|
+
[SpanKind.PRODUCER]: "producer",
|
|
229
|
+
[SpanKind.CONSUMER]: "consumer",
|
|
230
|
+
};
|
|
231
|
+
const STATUS_NAMES = {
|
|
232
|
+
[SpanStatusCode.UNSET]: "unset",
|
|
233
|
+
[SpanStatusCode.OK]: "ok",
|
|
234
|
+
[SpanStatusCode.ERROR]: "error",
|
|
235
|
+
};
|
|
236
|
+
/** The most attributes a span.end record carries, so one span never makes a huge line. */
|
|
237
|
+
const RECORD_ATTRIBUTES = 20;
|
|
238
|
+
function hrMillis(t) {
|
|
239
|
+
return t[0] * 1000 + t[1] / 1e6;
|
|
240
|
+
}
|
|
241
|
+
/** A W3C traceparent for a span context, always marked sampled: every trace is kept. */
|
|
242
|
+
export function traceparentOf(sc) {
|
|
243
|
+
return `00-${sc.traceId}-${sc.spanId}-01`;
|
|
244
|
+
}
|
|
245
|
+
function scopeOf(span) {
|
|
246
|
+
return span.instrumentationScope?.name ?? span.instrumentationLibrary?.name ?? "";
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Whether a span is written to the files: every span at a process boundary (server, client,
|
|
250
|
+
* producer, consumer), and the internal spans this service and this library open. A framework's
|
|
251
|
+
* own internal spans (a page render, a module resolution) stay in the backend: they are many, and
|
|
252
|
+
* not the story of the request.
|
|
253
|
+
*/
|
|
254
|
+
function writtenToFile(span) {
|
|
255
|
+
if (span.kind !== SpanKind.INTERNAL)
|
|
256
|
+
return true;
|
|
257
|
+
const scope = scopeOf(span);
|
|
258
|
+
return scope === obsConfig().service || scope === LIBRARY_SCOPE;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A span processor that writes one `span.end` record per span it keeps (see `writtenToFile`), with
|
|
262
|
+
* the span's own trace, span and parent ids, so the record sits in the tree where the span does.
|
|
263
|
+
* The record's fields: `span.name`, `span.kind`, `span.status`, `dur_ms`, `span.scope`, the span's
|
|
264
|
+
* links as traceparents (`span.links`), an error's type and message, and up to twenty of its
|
|
265
|
+
* attributes. Errors are ERROR records, so a level filter finds a failed span.
|
|
266
|
+
*/
|
|
267
|
+
export class FileSpanProcessor {
|
|
268
|
+
onStart() { }
|
|
269
|
+
onEnd(span) {
|
|
270
|
+
try {
|
|
271
|
+
if (!writtenToFile(span))
|
|
272
|
+
return;
|
|
273
|
+
const sc = span.spanContext();
|
|
274
|
+
const parent = span.parentSpanContext?.spanId ?? span.parentSpanId ?? "";
|
|
275
|
+
const ms = Math.round(hrMillis(span.duration) * 10) / 10;
|
|
276
|
+
const status = STATUS_NAMES[span.status.code] ?? "unset";
|
|
277
|
+
const fields = {
|
|
278
|
+
"span.name": span.name,
|
|
279
|
+
"span.kind": KIND_NAMES[span.kind] ?? "internal",
|
|
280
|
+
"span.status": status,
|
|
281
|
+
dur_ms: ms,
|
|
282
|
+
"span.scope": scopeOf(span),
|
|
283
|
+
};
|
|
284
|
+
if (span.links.length > 0) {
|
|
285
|
+
fields["span.links"] = span.links.map((l) => traceparentOf(l.context)).join(",");
|
|
286
|
+
}
|
|
287
|
+
const exception = span.events.find((e) => e.name === "exception");
|
|
288
|
+
if (status === "error") {
|
|
289
|
+
fields["error.type"] = String(exception?.attributes?.["exception.type"] ?? "error");
|
|
290
|
+
fields["error.message"] = String(exception?.attributes?.["exception.message"] ?? span.status.message ?? "");
|
|
291
|
+
}
|
|
292
|
+
let n = 0;
|
|
293
|
+
for (const [k, v] of Object.entries(span.attributes)) {
|
|
294
|
+
if (n >= RECORD_ATTRIBUTES)
|
|
295
|
+
break;
|
|
296
|
+
if (k in fields || v === undefined || typeof v === "object")
|
|
297
|
+
continue;
|
|
298
|
+
fields[k] = v;
|
|
299
|
+
n++;
|
|
300
|
+
}
|
|
301
|
+
counters.spansWritten++;
|
|
302
|
+
emitWith({ traceId: sc.traceId, spanId: sc.spanId, parentSpanId: parent }, {}, status === "error" ? "ERROR" : "INFO", "span", "span.end", `${span.name} ${status} in ${ms} ms`, fields);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
/* a span the processor cannot read is not the request's problem */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
forceFlush() {
|
|
309
|
+
return Promise.resolve();
|
|
310
|
+
}
|
|
311
|
+
shutdown() {
|
|
312
|
+
return Promise.resolve();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ---- context for work that outlives its request -----------------------------------------------
|
|
316
|
+
const TRACEPARENT = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
317
|
+
/**
|
|
318
|
+
* The span context a W3C traceparent names, or null for one that is missing, malformed, too long,
|
|
319
|
+
* or all zeros. A stored or annotated traceparent is untrusted input: it never fails the work that
|
|
320
|
+
* reads it, it only fails to be a parent.
|
|
321
|
+
*/
|
|
322
|
+
export function spanContextFromTraceparent(value) {
|
|
323
|
+
if (!value || value.length > 55)
|
|
324
|
+
return null;
|
|
325
|
+
const m = TRACEPARENT.exec(value.trim());
|
|
326
|
+
const traceId = m?.[1];
|
|
327
|
+
const spanId = m?.[2];
|
|
328
|
+
if (!traceId || !spanId || /^0+$/.test(traceId) || /^0+$/.test(spanId))
|
|
329
|
+
return null;
|
|
330
|
+
return { traceId, spanId, traceFlags: TraceFlags.SAMPLED, isRemote: true };
|
|
331
|
+
}
|
|
332
|
+
/** The traceparent of the span in force, to store on a row or an object for later work; "" outside one. */
|
|
333
|
+
export function currentTraceparent() {
|
|
334
|
+
const span = trace.getActiveSpan();
|
|
335
|
+
if (span?.isRecording())
|
|
336
|
+
return traceparentOf(span.spanContext());
|
|
337
|
+
return "";
|
|
338
|
+
}
|
|
339
|
+
const KINDS = {
|
|
340
|
+
internal: SpanKind.INTERNAL,
|
|
341
|
+
server: SpanKind.SERVER,
|
|
342
|
+
client: SpanKind.CLIENT,
|
|
343
|
+
producer: SpanKind.PRODUCER,
|
|
344
|
+
consumer: SpanKind.CONSUMER,
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Run work that another process asked for, or that a request asked for and is done later, as a
|
|
348
|
+
* span of its own: a child of the stored or annotated `parent` when there is one, else a new trace;
|
|
349
|
+
* linked to every traceparent in `links` (the other requests a batch serves, the operation an
|
|
350
|
+
* object was made for). A parent that is missing or malformed starts a new trace and never fails
|
|
351
|
+
* the work. An error is recorded on the span and thrown on.
|
|
352
|
+
*/
|
|
353
|
+
export async function continueFrom(parent, name, fn, opts) {
|
|
354
|
+
const sc = spanContextFromTraceparent(parent);
|
|
355
|
+
const ctx = sc ? trace.setSpanContext(ROOT_CONTEXT, sc) : ROOT_CONTEXT;
|
|
356
|
+
const links = [];
|
|
357
|
+
for (const l of opts?.links ?? []) {
|
|
358
|
+
const lc = spanContextFromTraceparent(l);
|
|
359
|
+
if (lc)
|
|
360
|
+
links.push({ context: lc });
|
|
361
|
+
}
|
|
362
|
+
const attributes = {};
|
|
363
|
+
for (const [k, v] of Object.entries(opts?.attributes ?? {})) {
|
|
364
|
+
if (v !== undefined && v !== null && typeof v !== "object")
|
|
365
|
+
attributes[k] = v;
|
|
366
|
+
}
|
|
367
|
+
const tracer = trace.getTracer(LIBRARY_SCOPE);
|
|
368
|
+
return otelContext.with(ctx, () => tracer.startActiveSpan(name, { kind: KINDS[opts?.kind ?? "internal"], links, attributes }, async (span) => {
|
|
369
|
+
// Without a tracer provider (a test, an edge runtime) the span does not record; the work
|
|
370
|
+
// still gets ids of its own, continuing the parent's trace.
|
|
371
|
+
const run = () => fn();
|
|
372
|
+
const body = span.isRecording()
|
|
373
|
+
? run
|
|
374
|
+
: () => runWithTrace({ traceId: sc?.traceId, parentSpanId: sc?.spanId }, run);
|
|
375
|
+
try {
|
|
376
|
+
const out = await body();
|
|
377
|
+
span.end();
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
span.recordException(e);
|
|
382
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: e?.message });
|
|
383
|
+
span.end();
|
|
384
|
+
throw e;
|
|
385
|
+
}
|
|
386
|
+
}));
|
|
387
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
// Severity vocabulary + numeric ordering. Matches the agent's stdlib levels
|
|
11
11
|
// (DEBUG < INFO < WARNING < ERROR < CRITICAL) so a LOG_LEVEL floor means the
|
|
12
|
-
// same thing
|
|
12
|
+
// same thing in either language.
|
|
13
13
|
export const LEVELS = {
|
|
14
14
|
DEBUG: 10,
|
|
15
15
|
INFO: 20,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diveinto/obs",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Structured logging, tracing and metrics for Next.js and Node services: one record shape, rotating files, W3C trace context, optional OpenTelemetry export",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@opentelemetry/api": "^1.9.1",
|
|
42
|
-
"@opentelemetry/api-logs": "
|
|
43
|
-
"@opentelemetry/exporter-logs-otlp-proto": "
|
|
42
|
+
"@opentelemetry/api-logs": ">=0.200.0 <0.300.0",
|
|
43
|
+
"@opentelemetry/exporter-logs-otlp-proto": ">=0.200.0 <0.300.0",
|
|
44
44
|
"@opentelemetry/resources": "^2.0.0",
|
|
45
|
-
"@opentelemetry/sdk-logs": "
|
|
46
|
-
"@vercel/otel": "^2.1.3"
|
|
45
|
+
"@opentelemetry/sdk-logs": ">=0.200.0 <0.300.0",
|
|
46
|
+
"@vercel/otel": "^2.1.3",
|
|
47
|
+
"@opentelemetry/sdk-trace-base": "^2.0.0"
|
|
47
48
|
},
|
|
48
49
|
"peerDependenciesMeta": {
|
|
49
50
|
"@opentelemetry/exporter-logs-otlp-proto": {
|
|
@@ -57,6 +58,9 @@
|
|
|
57
58
|
},
|
|
58
59
|
"@opentelemetry/resources": {
|
|
59
60
|
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@opentelemetry/sdk-trace-base": {
|
|
63
|
+
"optional": true
|
|
60
64
|
}
|
|
61
65
|
},
|
|
62
66
|
"devDependencies": {
|
|
@@ -68,7 +72,8 @@
|
|
|
68
72
|
"@types/node": "^22.10.2",
|
|
69
73
|
"@vercel/otel": "^2.1.3",
|
|
70
74
|
"typescript": "^5.7.2",
|
|
71
|
-
"vitest": "^3.0.5"
|
|
75
|
+
"vitest": "^3.0.5",
|
|
76
|
+
"@opentelemetry/sdk-trace-base": "^2.11.0"
|
|
72
77
|
},
|
|
73
78
|
"allowScripts": {
|
|
74
79
|
"esbuild@0.28.2": true
|