@farm.js/otel 0.1.0-beta.25 → 0.1.0-beta.27
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/dist/farm-events.d.ts +30 -0
- package/dist/farm-events.d.ts.map +1 -0
- package/dist/farm-events.js +264 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -1
- package/package.json +4 -3
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type MeterProvider } from "@opentelemetry/api";
|
|
2
|
+
/** Instrumentation scope shared by every instrument created here. */
|
|
3
|
+
export declare const FARM_EVENTS_METER_NAME = "@farm.js/otel";
|
|
4
|
+
export interface FarmEventMappingOptions {
|
|
5
|
+
/** Record counters and histograms for mapped events. Defaults to `true`. */
|
|
6
|
+
metrics?: boolean;
|
|
7
|
+
/** Add mapped events to the active span. Defaults to `true`. */
|
|
8
|
+
spanEvents?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Meter provider the instruments are created from. Defaults to the globally
|
|
11
|
+
* registered provider, which only becomes real once an SDK has started, so
|
|
12
|
+
* instruments must be created after `sdk.start()`.
|
|
13
|
+
*/
|
|
14
|
+
meterProvider?: MeterProvider;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Subscribes to Farm's observability bus and records the framework's own cache,
|
|
18
|
+
* PPR, streaming and middleware signals as OpenTelemetry metrics plus events on
|
|
19
|
+
* the active span.
|
|
20
|
+
*
|
|
21
|
+
* Metric attributes are deliberately limited to bounded values. A cache `key`
|
|
22
|
+
* embeds the serialized arguments of the cached call and a `tag` is authored at
|
|
23
|
+
* runtime, so neither is ever used as a metric attribute; per-request detail
|
|
24
|
+
* stays on span events, where high cardinality is free.
|
|
25
|
+
*
|
|
26
|
+
* Returns a dispose function. Call it during shutdown so a development restart
|
|
27
|
+
* does not stack subscribers.
|
|
28
|
+
*/
|
|
29
|
+
export declare function recordFarmEvents(options?: FarmEventMappingOptions): Promise<() => void>;
|
|
30
|
+
//# sourceMappingURL=farm-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"farm-events.d.ts","sourceRoot":"","sources":["../src/farm-events.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAEnB,MAAM,oBAAoB,CAAC;AAG5B,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAEtD,MAAM,WAAW,uBAAuB;IACtC,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAmDjG"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { metrics as metricsApi, trace, } from "@opentelemetry/api";
|
|
2
|
+
/** Instrumentation scope shared by every instrument created here. */
|
|
3
|
+
export const FARM_EVENTS_METER_NAME = "@farm.js/otel";
|
|
4
|
+
const CACHE_LOOKUP_RESULTS = {
|
|
5
|
+
"cache.hit": "hit",
|
|
6
|
+
"cache.miss": "miss",
|
|
7
|
+
"cache.stale": "stale",
|
|
8
|
+
"cache.dedupe": "dedupe",
|
|
9
|
+
"cache.bypass": "bypass",
|
|
10
|
+
};
|
|
11
|
+
const CACHE_INVALIDATION_OPERATIONS = {
|
|
12
|
+
"cache.revalidateTag": "revalidateTag",
|
|
13
|
+
"cache.updateTag": "updateTag",
|
|
14
|
+
"cache.revalidatePath": "revalidatePath",
|
|
15
|
+
"cache.invalidated": "invalidated",
|
|
16
|
+
};
|
|
17
|
+
const PPR_SHELL_RESULTS = {
|
|
18
|
+
"ppr.shell.hit": "hit",
|
|
19
|
+
"ppr.shell.miss": "miss",
|
|
20
|
+
"ppr.shell.cached": "cached",
|
|
21
|
+
"ppr.shell.bypass": "bypass",
|
|
22
|
+
"ppr.shell.invalidated": "invalidated",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Subscribes to Farm's observability bus and records the framework's own cache,
|
|
26
|
+
* PPR, streaming and middleware signals as OpenTelemetry metrics plus events on
|
|
27
|
+
* the active span.
|
|
28
|
+
*
|
|
29
|
+
* Metric attributes are deliberately limited to bounded values. A cache `key`
|
|
30
|
+
* embeds the serialized arguments of the cached call and a `tag` is authored at
|
|
31
|
+
* runtime, so neither is ever used as a metric attribute; per-request detail
|
|
32
|
+
* stays on span events, where high cardinality is free.
|
|
33
|
+
*
|
|
34
|
+
* Returns a dispose function. Call it during shutdown so a development restart
|
|
35
|
+
* does not stack subscribers.
|
|
36
|
+
*/
|
|
37
|
+
export async function recordFarmEvents(options = {}) {
|
|
38
|
+
const recordMetrics = options.metrics !== false;
|
|
39
|
+
const recordSpanEvents = options.spanEvents !== false;
|
|
40
|
+
if (!recordMetrics && !recordSpanEvents)
|
|
41
|
+
return () => { };
|
|
42
|
+
let subscribe;
|
|
43
|
+
try {
|
|
44
|
+
// Imported lazily: `registerOTel` runs from `instrumentation.ts` before the
|
|
45
|
+
// application loads, so the pre-start module graph stays limited to the SDK,
|
|
46
|
+
// and a core that cannot be resolved costs telemetry rather than the server.
|
|
47
|
+
({ onFarmEvent: subscribe } = await import("@farm.js/core/observability"));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.warn(`[farm:otel] farm events are unavailable, continuing without them: ${describe(error)}`);
|
|
51
|
+
return () => { };
|
|
52
|
+
}
|
|
53
|
+
let instruments;
|
|
54
|
+
if (recordMetrics) {
|
|
55
|
+
try {
|
|
56
|
+
const provider = options.meterProvider ?? metricsApi.getMeterProvider();
|
|
57
|
+
instruments = createFarmEventInstruments(provider.getMeter(FARM_EVENTS_METER_NAME));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// A meter provider that cannot hand out instruments must not stop the
|
|
61
|
+
// server from starting.
|
|
62
|
+
console.warn(`[farm:otel] failed to create farm metrics, continuing without them: ${describe(error)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!instruments && !recordSpanEvents)
|
|
66
|
+
return () => { };
|
|
67
|
+
let warned = false;
|
|
68
|
+
return subscribe((event) => {
|
|
69
|
+
try {
|
|
70
|
+
handleFarmEvent(event, instruments, recordSpanEvents);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// Telemetry must never reach the emitting request. Warn once, because the
|
|
74
|
+
// failing event families are emitted on hot paths.
|
|
75
|
+
if (warned)
|
|
76
|
+
return;
|
|
77
|
+
warned = true;
|
|
78
|
+
console.warn(`[farm:otel] failed to record farm events: ${describe(error)}`);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
// `observability.events` narrows which events reach application handlers and
|
|
82
|
+
// logs. Instruments are not a log drain, and Farm already records span
|
|
83
|
+
// events regardless of that filter, so the mapping stays consistent with it.
|
|
84
|
+
{ unfiltered: true });
|
|
85
|
+
}
|
|
86
|
+
function createFarmEventInstruments(meter) {
|
|
87
|
+
return {
|
|
88
|
+
cacheLookups: meter.createCounter("farm.cache.lookups", {
|
|
89
|
+
description: "Farm cache lookups by outcome.",
|
|
90
|
+
unit: "{lookup}",
|
|
91
|
+
}),
|
|
92
|
+
cacheInvalidations: meter.createCounter("farm.cache.invalidations", {
|
|
93
|
+
description: "Farm cache invalidation operations by kind.",
|
|
94
|
+
unit: "{invalidation}",
|
|
95
|
+
}),
|
|
96
|
+
cacheInvalidatedEntries: meter.createCounter("farm.cache.invalidated_entries", {
|
|
97
|
+
description: "Cache entries removed by Farm cache invalidations.",
|
|
98
|
+
unit: "{entry}",
|
|
99
|
+
}),
|
|
100
|
+
pprShellLookups: meter.createCounter("farm.ppr.shell.lookups", {
|
|
101
|
+
description: "Partial prerender shell lookups by outcome.",
|
|
102
|
+
unit: "{lookup}",
|
|
103
|
+
}),
|
|
104
|
+
pprRefreshDuration: meter.createHistogram("farm.ppr.refresh.duration", {
|
|
105
|
+
description: "Duration of completed partial prerender shell refreshes.",
|
|
106
|
+
unit: "ms",
|
|
107
|
+
}),
|
|
108
|
+
shellDuration: meter.createHistogram("farm.render.stream.shell.duration", {
|
|
109
|
+
description: "Time from stream start to streaming shell ready.",
|
|
110
|
+
unit: "ms",
|
|
111
|
+
}),
|
|
112
|
+
middlewareShortCircuits: meter.createCounter("farm.middleware.short_circuits", {
|
|
113
|
+
description: "Requests answered by middleware before reaching a route.",
|
|
114
|
+
unit: "{short_circuit}",
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function handleFarmEvent(event, instruments, recordSpanEvents) {
|
|
119
|
+
const record = event;
|
|
120
|
+
switch (event.type) {
|
|
121
|
+
case "cache.hit":
|
|
122
|
+
case "cache.miss":
|
|
123
|
+
case "cache.stale":
|
|
124
|
+
case "cache.dedupe":
|
|
125
|
+
case "cache.bypass": {
|
|
126
|
+
const result = CACHE_LOOKUP_RESULTS[event.type];
|
|
127
|
+
const stale = event.type === "cache.hit" && event.stale === true;
|
|
128
|
+
const attributes = { "farm.cache.result": result };
|
|
129
|
+
if (stale)
|
|
130
|
+
attributes["farm.cache.stale"] = true;
|
|
131
|
+
instruments?.cacheLookups.add(1, attributes);
|
|
132
|
+
const span = spanForEvent(event, recordSpanEvents);
|
|
133
|
+
if (!span)
|
|
134
|
+
return;
|
|
135
|
+
const spanAttributes = { ...attributes };
|
|
136
|
+
copyAttribute(spanAttributes, "farm.route", record.route);
|
|
137
|
+
copyAttribute(spanAttributes, "farm.pathname", record.pathname);
|
|
138
|
+
copyAttribute(spanAttributes, "farm.tags", record.tags);
|
|
139
|
+
copyAttribute(spanAttributes, "farm.reason", record.reason);
|
|
140
|
+
copyAttribute(spanAttributes, "farm.revalidate", record.revalidate);
|
|
141
|
+
span.addEvent(event.type, spanAttributes, event.timestamp);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
case "cache.revalidateTag":
|
|
145
|
+
case "cache.updateTag":
|
|
146
|
+
case "cache.revalidatePath":
|
|
147
|
+
case "cache.invalidated": {
|
|
148
|
+
const operation = CACHE_INVALIDATION_OPERATIONS[event.type];
|
|
149
|
+
const attributes = { "farm.cache.operation": operation };
|
|
150
|
+
instruments?.cacheInvalidations.add(1, attributes);
|
|
151
|
+
const count = record.count;
|
|
152
|
+
if (typeof count === "number" && Number.isFinite(count) && count > 0) {
|
|
153
|
+
instruments?.cacheInvalidatedEntries.add(count, attributes);
|
|
154
|
+
}
|
|
155
|
+
const span = spanForEvent(event, recordSpanEvents);
|
|
156
|
+
if (!span)
|
|
157
|
+
return;
|
|
158
|
+
const spanAttributes = { ...attributes };
|
|
159
|
+
copyAttribute(spanAttributes, "farm.route", record.route);
|
|
160
|
+
copyAttribute(spanAttributes, "farm.tag", record.tag);
|
|
161
|
+
copyAttribute(spanAttributes, "farm.path", record.path);
|
|
162
|
+
copyAttribute(spanAttributes, "farm.reason", record.reason);
|
|
163
|
+
copyAttribute(spanAttributes, "farm.count", count);
|
|
164
|
+
span.addEvent(event.type, spanAttributes, event.timestamp);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
case "ppr.shell.hit":
|
|
168
|
+
case "ppr.shell.miss":
|
|
169
|
+
case "ppr.shell.cached":
|
|
170
|
+
case "ppr.shell.bypass":
|
|
171
|
+
case "ppr.shell.invalidated": {
|
|
172
|
+
const result = PPR_SHELL_RESULTS[event.type];
|
|
173
|
+
const attributes = { "farm.ppr.result": result };
|
|
174
|
+
instruments?.pprShellLookups.add(1, attributes);
|
|
175
|
+
const span = spanForEvent(event, recordSpanEvents);
|
|
176
|
+
if (!span)
|
|
177
|
+
return;
|
|
178
|
+
const spanAttributes = { ...attributes };
|
|
179
|
+
copyAttribute(spanAttributes, "farm.route", record.route);
|
|
180
|
+
copyAttribute(spanAttributes, "farm.reason", record.reason);
|
|
181
|
+
copyAttribute(spanAttributes, "farm.revalidate", record.revalidate);
|
|
182
|
+
copyAttribute(spanAttributes, "farm.count", record.count);
|
|
183
|
+
span.addEvent(event.type, spanAttributes, event.timestamp);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
case "ppr.refresh.complete": {
|
|
187
|
+
instruments?.pprRefreshDuration.record(event.durationMs);
|
|
188
|
+
const span = spanForEvent(event, recordSpanEvents);
|
|
189
|
+
if (!span)
|
|
190
|
+
return;
|
|
191
|
+
const spanAttributes = { "farm.durationMs": event.durationMs };
|
|
192
|
+
copyAttribute(spanAttributes, "farm.route", record.route);
|
|
193
|
+
span.addEvent(event.type, spanAttributes, event.timestamp);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
case "render.stream.shellReady": {
|
|
197
|
+
instruments?.shellDuration.record(event.durationMs);
|
|
198
|
+
const span = spanForEvent(event, recordSpanEvents);
|
|
199
|
+
if (!span)
|
|
200
|
+
return;
|
|
201
|
+
const spanAttributes = { "farm.durationMs": event.durationMs };
|
|
202
|
+
copyAttribute(spanAttributes, "farm.route", record.route);
|
|
203
|
+
span.addEvent(event.type, spanAttributes, event.timestamp);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
case "middleware.shortCircuit": {
|
|
207
|
+
const attributes = {};
|
|
208
|
+
// A middleware `route` is the configured matcher, not the request path,
|
|
209
|
+
// so unlike every other family here it is bounded by the application's
|
|
210
|
+
// own middleware list and is safe to keep on the counter.
|
|
211
|
+
copyAttribute(attributes, "farm.route", record.route);
|
|
212
|
+
const statusClass = toStatusClass(event.status);
|
|
213
|
+
if (statusClass)
|
|
214
|
+
attributes["farm.http.status_class"] = statusClass;
|
|
215
|
+
instruments?.middlewareShortCircuits.add(1, attributes);
|
|
216
|
+
const span = spanForEvent(event, recordSpanEvents);
|
|
217
|
+
if (!span)
|
|
218
|
+
return;
|
|
219
|
+
const spanAttributes = { ...attributes };
|
|
220
|
+
copyAttribute(spanAttributes, "farm.pathname", record.pathname);
|
|
221
|
+
copyAttribute(spanAttributes, "farm.name", record.name);
|
|
222
|
+
copyAttribute(spanAttributes, "farm.status", event.status);
|
|
223
|
+
span.addEvent(event.type, spanAttributes, event.timestamp);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The span a mapped event should be recorded on, if any. Resolved before the
|
|
232
|
+
* attributes are collected so the common case, an event emitted with no
|
|
233
|
+
* recording span active, costs nothing.
|
|
234
|
+
*/
|
|
235
|
+
function spanForEvent(event, recordSpanEvents) {
|
|
236
|
+
if (!recordSpanEvents)
|
|
237
|
+
return undefined;
|
|
238
|
+
// Farm's own tracing layer already writes every event onto the active span
|
|
239
|
+
// when `observability.tracing` is enabled, and stamps the event with the span
|
|
240
|
+
// it recorded against. Adding a second copy here would duplicate the trace.
|
|
241
|
+
if (event.spanId)
|
|
242
|
+
return undefined;
|
|
243
|
+
const span = trace.getActiveSpan();
|
|
244
|
+
return span?.isRecording() ? span : undefined;
|
|
245
|
+
}
|
|
246
|
+
function copyAttribute(target, name, value) {
|
|
247
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
248
|
+
target[name] = value;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
|
|
252
|
+
target[name] = [...value];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function toStatusClass(status) {
|
|
256
|
+
if (typeof status !== "number" || !Number.isFinite(status))
|
|
257
|
+
return undefined;
|
|
258
|
+
if (status < 100 || status > 599)
|
|
259
|
+
return undefined;
|
|
260
|
+
return `${Math.floor(status / 100)}xx`;
|
|
261
|
+
}
|
|
262
|
+
function describe(error) {
|
|
263
|
+
return error instanceof Error ? error.message : String(error);
|
|
264
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Attributes } from "@opentelemetry/api";
|
|
2
2
|
import { type InstrumentationConfigMap } from "@opentelemetry/auto-instrumentations-node";
|
|
3
3
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
4
4
|
import type { Instrumentation } from "@opentelemetry/instrumentation";
|
|
5
5
|
import { type Resource } from "@opentelemetry/resources";
|
|
6
6
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
7
7
|
import { type Sampler, type SpanExporter, type SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
8
|
+
import { type FarmEventMappingOptions } from "./farm-events.js";
|
|
9
|
+
export { FARM_EVENTS_METER_NAME, recordFarmEvents } from "./farm-events.js";
|
|
10
|
+
export type { FarmEventMappingOptions } from "./farm-events.js";
|
|
8
11
|
export interface FarmOTelOptions {
|
|
9
12
|
serviceName?: string;
|
|
10
13
|
serviceVersion?: string;
|
|
@@ -17,6 +20,22 @@ export interface FarmOTelOptions {
|
|
|
17
20
|
autoInstrumentations?: boolean;
|
|
18
21
|
instrumentationConfig?: InstrumentationConfigMap;
|
|
19
22
|
instrumentations?: Array<Instrumentation | Instrumentation[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Metric readers to export with. Farm registers a meter provider only when
|
|
25
|
+
* this is set or `OTEL_METRICS_EXPORTER` names an exporter, so an app that
|
|
26
|
+
* only asked for traces never gets a metrics pipeline it did not configure.
|
|
27
|
+
*/
|
|
28
|
+
metricReaders?: NonNullable<ConstructorParameters<typeof NodeSDK>[0]>["metricReaders"];
|
|
29
|
+
/**
|
|
30
|
+
* Record Farm's own cache, PPR, streaming and middleware events as metrics
|
|
31
|
+
* and span events. Enabled by default; auto-instrumentation only sees generic
|
|
32
|
+
* HTTP, filesystem and database work, so without this everything the
|
|
33
|
+
* framework knows about itself is dropped.
|
|
34
|
+
*
|
|
35
|
+
* Pass `false` to leave Farm's event bus unsubscribed, or an object to record
|
|
36
|
+
* only metrics or only span events.
|
|
37
|
+
*/
|
|
38
|
+
farmEvents?: boolean | FarmEventMappingOptions;
|
|
20
39
|
}
|
|
21
40
|
export interface FarmOTelController {
|
|
22
41
|
sdk: NodeSDK;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAA0B,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAEL,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAoB,KAAK,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAElF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,YAAY,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,kBAAkB,CAAC,EAAE,UAAU,CAAC;IAChC,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,EAAE,qBAAqB,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,wBAAwB,CAAC;IACjD,gBAAgB,CAAC,EAAE,KAAK,CAAC,eAAe,GAAG,eAAe,EAAE,CAAC,CAAC;IAC9D;;;;OAIG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,qBAAqB,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;IACvF;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAC;CAChD;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,OAAO,CAAC;IACb,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAwBD,wBAAsB,YAAY,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAqG7F;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAIpD;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAIlD"}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
import { metrics } from "@opentelemetry/api";
|
|
1
2
|
import { getNodeAutoInstrumentations, } from "@opentelemetry/auto-instrumentations-node";
|
|
2
3
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
3
4
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
5
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
5
6
|
import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
|
|
6
7
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
8
|
+
import { recordFarmEvents } from "./farm-events.js";
|
|
9
|
+
export { FARM_EVENTS_METER_NAME, recordFarmEvents } from "./farm-events.js";
|
|
7
10
|
const FARM_OTEL_STATE = Symbol.for("@farm.js/otel/state");
|
|
8
11
|
function getGlobalState() {
|
|
9
12
|
const target = globalThis;
|
|
10
13
|
return (target[FARM_OTEL_STATE] ??= {});
|
|
11
14
|
}
|
|
15
|
+
function resolveFarmEventOptions(option) {
|
|
16
|
+
if (option === false)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (option === undefined || option === true)
|
|
19
|
+
return {};
|
|
20
|
+
return option;
|
|
21
|
+
}
|
|
12
22
|
export async function registerOTel(options = {}) {
|
|
13
23
|
const state = getGlobalState();
|
|
14
24
|
if (state.controller)
|
|
@@ -16,7 +26,7 @@ export async function registerOTel(options = {}) {
|
|
|
16
26
|
if (state.initializing)
|
|
17
27
|
return state.initializing;
|
|
18
28
|
state.initializing = Promise.resolve()
|
|
19
|
-
.then(() => {
|
|
29
|
+
.then(async () => {
|
|
20
30
|
const defaultResource = resourceFromAttributes({
|
|
21
31
|
[ATTR_SERVICE_NAME]: options.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "farm-app",
|
|
22
32
|
...(options.serviceVersion ? { [ATTR_SERVICE_VERSION]: options.serviceVersion } : {}),
|
|
@@ -30,21 +40,61 @@ export async function registerOTel(options = {}) {
|
|
|
30
40
|
(options.autoInstrumentations === false
|
|
31
41
|
? []
|
|
32
42
|
: [getNodeAutoInstrumentations(options.instrumentationConfig)]);
|
|
43
|
+
// Left unset, the Node SDK falls back to a periodic OTLP metrics pipeline
|
|
44
|
+
// pointed at localhost:4318. With nothing listening there, the first
|
|
45
|
+
// recorded metric turns graceful shutdown into the exporter's retry window
|
|
46
|
+
// (measured at ~8s), and farm's production lifecycle waits on
|
|
47
|
+
// instrumentation shutdown, so that is a stalled deploy.
|
|
48
|
+
//
|
|
49
|
+
// An empty reader list keeps the meter provider unregistered, which turns
|
|
50
|
+
// metrics off entirely, so it may only be the default when the app has
|
|
51
|
+
// pointed nothing at a collector. `OTEL_METRICS_EXPORTER` alone is not that
|
|
52
|
+
// signal: the spec already defaults it to `otlp`, so the ordinary
|
|
53
|
+
// production posture is to set an endpoint and leave the exporter variable
|
|
54
|
+
// alone. Treat any configured OTLP metrics destination as the opt-in, and
|
|
55
|
+
// leave `OTEL_METRICS_EXPORTER=none` as the documented way to turn metrics
|
|
56
|
+
// off.
|
|
57
|
+
const metricsDestinationConfigured = Boolean(process.env.OTEL_METRICS_EXPORTER ||
|
|
58
|
+
process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ||
|
|
59
|
+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
|
|
60
|
+
const metricReaders = options.metricReaders ?? (metricsDestinationConfigured ? undefined : []);
|
|
33
61
|
const sdk = new NodeSDK({
|
|
34
62
|
resource,
|
|
35
63
|
sampler: options.sampler,
|
|
36
64
|
spanProcessors,
|
|
37
65
|
instrumentations,
|
|
66
|
+
metricReaders,
|
|
38
67
|
});
|
|
39
68
|
sdk.start();
|
|
69
|
+
// After `start`, because the metrics API has no proxy provider: an
|
|
70
|
+
// instrument created before the SDK registers its meter provider stays a
|
|
71
|
+
// no-op for the life of the process.
|
|
72
|
+
const farmEventOptions = resolveFarmEventOptions(options.farmEvents);
|
|
73
|
+
const disposeFarmEvents = farmEventOptions
|
|
74
|
+
? await recordFarmEvents(farmEventOptions)
|
|
75
|
+
: undefined;
|
|
40
76
|
let shutdownPromise;
|
|
41
77
|
const controller = {
|
|
42
78
|
sdk,
|
|
43
79
|
async forceFlush() {
|
|
44
80
|
await Promise.all(spanProcessors.map((processor) => processor.forceFlush()));
|
|
81
|
+
// Serverless runtimes freeze the process between requests, so the
|
|
82
|
+
// periodic metric reader never gets to export on its own. The API type
|
|
83
|
+
// has no `forceFlush`, so reach for the SDK's when it is there.
|
|
84
|
+
const meterProvider = metrics.getMeterProvider();
|
|
85
|
+
try {
|
|
86
|
+
await meterProvider.forceFlush?.();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// A metrics exporter must not fail the response it was flushed for.
|
|
90
|
+
console.warn(`[farm:otel] metric flush failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
|
+
}
|
|
45
92
|
},
|
|
46
93
|
shutdown() {
|
|
47
94
|
if (!shutdownPromise) {
|
|
95
|
+
// Stop feeding instruments before the providers go away, and so a
|
|
96
|
+
// development restart does not stack bus subscribers.
|
|
97
|
+
disposeFarmEvents?.();
|
|
48
98
|
shutdownPromise = sdk.shutdown().finally(() => {
|
|
49
99
|
state.controller = undefined;
|
|
50
100
|
state.initializing = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farm.js/otel",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.27",
|
|
4
4
|
"description": "OpenTelemetry SDK setup for Farm.js applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,14 +33,15 @@
|
|
|
33
33
|
"@opentelemetry/resources": "2.10.0",
|
|
34
34
|
"@opentelemetry/sdk-node": "0.221.0",
|
|
35
35
|
"@opentelemetry/sdk-trace-base": "2.10.0",
|
|
36
|
-
"@opentelemetry/semantic-conventions": "1.43.0"
|
|
36
|
+
"@opentelemetry/semantic-conventions": "1.43.0",
|
|
37
|
+
"@farm.js/core": "0.1.0-beta.107"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/node": "^20.10.5",
|
|
40
41
|
"typescript": "^5.3.3"
|
|
41
42
|
},
|
|
42
43
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
44
|
+
"node": ">=22.13.0"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|