react-on-rails-pro-node-renderer 16.7.0-rc.2 → 16.7.0-rc.3
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/lib/integrations/api.d.ts +2 -2
- package/lib/integrations/api.js +5 -3
- package/lib/integrations/opentelemetry.d.ts +34 -0
- package/lib/integrations/opentelemetry.js +305 -0
- package/lib/shared/opentelemetryState.d.ts +4 -0
- package/lib/shared/opentelemetryState.js +12 -0
- package/lib/shared/tracing.d.ts +80 -1
- package/lib/shared/tracing.js +109 -1
- package/lib/testUtils/opentelemetry.d.ts +2 -0
- package/lib/testUtils/opentelemetry.js +66 -0
- package/lib/worker/fastifyConfig.d.ts +19 -0
- package/lib/worker/fastifyConfig.js +41 -0
- package/lib/worker/handleGracefulShutdown.js +50 -3
- package/lib/worker/handleIncrementalRenderRequest.js +22 -5
- package/lib/worker/handleIncrementalRenderStream.js +104 -101
- package/lib/worker/handleRenderRequest.d.ts +2 -1
- package/lib/worker/handleRenderRequest.js +82 -29
- package/lib/worker/shutdownHooks.d.ts +12 -0
- package/lib/worker/shutdownHooks.js +41 -0
- package/lib/worker.d.ts +1 -9
- package/lib/worker.js +84 -77
- package/package.json +48 -3
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
*/
|
|
25
25
|
export { default as log } from '../shared/log.js';
|
|
26
26
|
export { addErrorNotifier, addMessageNotifier, addNotifier, error, message, Notifier, ErrorNotifier, MessageNotifier, } from '../shared/errorReporter.js';
|
|
27
|
-
export { setupTracing, TracingContext, TracingIntegrationOptions, UnitOfWorkOptions, } from '../shared/tracing.js';
|
|
28
|
-
export { configureFastify, FastifyConfigFunction } from '../worker.js';
|
|
27
|
+
export { setupTracing, setupSubSpan, subSpan, TracingContext, TracingIntegrationOptions, UnitOfWorkOptions, SubSpanOptions, SubSpanFn, SubSpanController, } from '../shared/tracing.js';
|
|
28
|
+
export { configureFastify, FastifyConfigFunction } from '../worker/fastifyConfig.js';
|
|
29
29
|
//# sourceMappingURL=api.d.ts.map
|
package/lib/integrations/api.js
CHANGED
|
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
27
27
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
28
|
};
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
-
exports.configureFastify = exports.setupTracing = exports.message = exports.error = exports.addNotifier = exports.addMessageNotifier = exports.addErrorNotifier = exports.log = void 0;
|
|
30
|
+
exports.configureFastify = exports.subSpan = exports.setupSubSpan = exports.setupTracing = exports.message = exports.error = exports.addNotifier = exports.addMessageNotifier = exports.addErrorNotifier = exports.log = void 0;
|
|
31
31
|
var log_js_1 = require("../shared/log.js");
|
|
32
32
|
Object.defineProperty(exports, "log", { enumerable: true, get: function () { return __importDefault(log_js_1).default; } });
|
|
33
33
|
var errorReporter_js_1 = require("../shared/errorReporter.js");
|
|
@@ -38,6 +38,8 @@ Object.defineProperty(exports, "error", { enumerable: true, get: function () { r
|
|
|
38
38
|
Object.defineProperty(exports, "message", { enumerable: true, get: function () { return errorReporter_js_1.message; } });
|
|
39
39
|
var tracing_js_1 = require("../shared/tracing.js");
|
|
40
40
|
Object.defineProperty(exports, "setupTracing", { enumerable: true, get: function () { return tracing_js_1.setupTracing; } });
|
|
41
|
-
|
|
42
|
-
Object.defineProperty(exports, "
|
|
41
|
+
Object.defineProperty(exports, "setupSubSpan", { enumerable: true, get: function () { return tracing_js_1.setupSubSpan; } });
|
|
42
|
+
Object.defineProperty(exports, "subSpan", { enumerable: true, get: function () { return tracing_js_1.subSpan; } });
|
|
43
|
+
var fastifyConfig_js_1 = require("../worker/fastifyConfig.js");
|
|
44
|
+
Object.defineProperty(exports, "configureFastify", { enumerable: true, get: function () { return fastifyConfig_js_1.configureFastify; } });
|
|
43
45
|
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Attributes } from '@opentelemetry/api';
|
|
2
|
+
import type { SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
declare module '../shared/tracing.js' {
|
|
4
|
+
interface UnitOfWorkOptions {
|
|
5
|
+
opentelemetry?: {
|
|
6
|
+
name: string;
|
|
7
|
+
attributes?: Attributes;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export interface OpenTelemetryInitOptions {
|
|
12
|
+
/** Service name reported in traces. Defaults to "react-on-rails-pro-node-renderer".
|
|
13
|
+
* `OTEL_SERVICE_NAME` env var takes precedence over this value. If neither is
|
|
14
|
+
* set, `resourceAttributes["service.name"]` or `OTEL_RESOURCE_ATTRIBUTES`
|
|
15
|
+
* can override the default service name. */
|
|
16
|
+
serviceName?: string;
|
|
17
|
+
/** Register HTTP + Fastify auto-instrumentation. Default: false.
|
|
18
|
+
* OpenTelemetry module patches are process-global and cannot be rolled back;
|
|
19
|
+
* if later init steps fail, patched modules remain installed with a no-op tracer. */
|
|
20
|
+
fastify?: boolean;
|
|
21
|
+
/** Wrap SSR work in spans via setupTracing + setupSubSpan. Default: false. */
|
|
22
|
+
tracing?: boolean;
|
|
23
|
+
/** Override the default OTLP HTTP exporter. */
|
|
24
|
+
exporter?: SpanExporter;
|
|
25
|
+
/** Override the default span processor.
|
|
26
|
+
* Default: BatchSpanProcessor in production, SimpleSpanProcessor otherwise. */
|
|
27
|
+
spanProcessor?: SpanProcessor;
|
|
28
|
+
/** Additional resource attributes merged into the default resource. */
|
|
29
|
+
resourceAttributes?: Record<string, string>;
|
|
30
|
+
/** Maximum time to wait for provider.shutdown() during Fastify onClose. Default: 5000ms. */
|
|
31
|
+
shutdownTimeoutMs?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function init(opts?: OpenTelemetryInitOptions): void;
|
|
34
|
+
//# sourceMappingURL=opentelemetry.d.ts.map
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.init = init;
|
|
4
|
+
/* eslint-disable no-restricted-imports --
|
|
5
|
+
* This integration needs internal worker/shared modules that the public
|
|
6
|
+
* api.ts does not yet re-export (tracing adapter slots, OTel global state,
|
|
7
|
+
* fastify config + worker shutdown hook registration). Tracked in #3419
|
|
8
|
+
* — once api.ts surfaces these, remove this disable. */
|
|
9
|
+
const tracing_js_1 = require("../shared/tracing.js");
|
|
10
|
+
const opentelemetryState_js_1 = require("../shared/opentelemetryState.js");
|
|
11
|
+
const fastifyConfig_js_1 = require("../worker/fastifyConfig.js");
|
|
12
|
+
const shutdownHooks_js_1 = require("../worker/shutdownHooks.js");
|
|
13
|
+
/* eslint-enable no-restricted-imports */
|
|
14
|
+
const api_js_1 = require("./api.js");
|
|
15
|
+
const DEFAULT_SERVICE_NAME = 'react-on-rails-pro-node-renderer';
|
|
16
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
|
|
17
|
+
// Leave 1s of headroom under the worker's hard cap so the shutdown hook can
|
|
18
|
+
// resolve cleanly even when provider.shutdown() runs right at its limit.
|
|
19
|
+
const MAX_SHUTDOWN_TIMEOUT_MS = shutdownHooks_js_1.WORKER_SHUTDOWN_HOOKS_TIMEOUT_MS - 1000;
|
|
20
|
+
function isProduction() {
|
|
21
|
+
return process.env.NODE_ENV === 'production' || process.env.RAILS_ENV === 'production';
|
|
22
|
+
}
|
|
23
|
+
function resolveConfiguredServiceName(opts) {
|
|
24
|
+
return process.env.OTEL_SERVICE_NAME ?? opts.serviceName;
|
|
25
|
+
}
|
|
26
|
+
function resolveShutdownTimeoutMs(opts) {
|
|
27
|
+
const requested = opts.shutdownTimeoutMs;
|
|
28
|
+
if (requested === undefined) {
|
|
29
|
+
return DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
30
|
+
}
|
|
31
|
+
if (!Number.isFinite(requested) || requested <= 0) {
|
|
32
|
+
return DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
33
|
+
}
|
|
34
|
+
if (requested > MAX_SHUTDOWN_TIMEOUT_MS) {
|
|
35
|
+
api_js_1.log.warn('[OpenTelemetry] shutdownTimeoutMs=%dms exceeds worker shutdown hook cap (%dms); capping to %dms so the hook can resolve before the worker is forcibly destroyed.', requested, shutdownHooks_js_1.WORKER_SHUTDOWN_HOOKS_TIMEOUT_MS, MAX_SHUTDOWN_TIMEOUT_MS);
|
|
36
|
+
return MAX_SHUTDOWN_TIMEOUT_MS;
|
|
37
|
+
}
|
|
38
|
+
return requested;
|
|
39
|
+
}
|
|
40
|
+
function parseResourceAttributes(value) {
|
|
41
|
+
if (!value)
|
|
42
|
+
return {};
|
|
43
|
+
// OTel resource attributes are comma-separated. Literal commas in values must
|
|
44
|
+
// be percent-encoded by callers; unencoded commas split the value.
|
|
45
|
+
const attributes = {};
|
|
46
|
+
for (const pair of value.split(',')) {
|
|
47
|
+
const [rawKey, ...rawValueParts] = pair.split('=');
|
|
48
|
+
const key = rawKey?.trim();
|
|
49
|
+
if (key && rawValueParts.length > 0) {
|
|
50
|
+
const rawValue = rawValueParts.join('=').trim().replace(/^"|"$/g, '');
|
|
51
|
+
try {
|
|
52
|
+
attributes[key] = decodeURIComponent(rawValue);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Keep init resilient when callers provide malformed percent-encoding.
|
|
56
|
+
attributes[key] = rawValue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return attributes;
|
|
61
|
+
}
|
|
62
|
+
function configureOpenTelemetryDiagnostics(otelApi) {
|
|
63
|
+
otelApi.diag.setLogger({
|
|
64
|
+
error: (diagnosticMessage, ...args) => api_js_1.log.error({ otel: true, level: 'error', args }, diagnosticMessage),
|
|
65
|
+
warn: (diagnosticMessage, ...args) => api_js_1.log.warn({ otel: true, level: 'warn', args }, diagnosticMessage),
|
|
66
|
+
// DiagLogLevel.WARN below suppresses lower-severity diagnostics before
|
|
67
|
+
// these callbacks run. Keep no-op methods to satisfy the OTel logger API.
|
|
68
|
+
info: () => undefined,
|
|
69
|
+
debug: () => undefined,
|
|
70
|
+
verbose: () => undefined,
|
|
71
|
+
}, otelApi.DiagLogLevel.WARN);
|
|
72
|
+
}
|
|
73
|
+
function disableOpenTelemetryGlobals(otelApi) {
|
|
74
|
+
otelApi.trace.disable();
|
|
75
|
+
otelApi.context.disable();
|
|
76
|
+
otelApi.propagation.disable();
|
|
77
|
+
otelApi.diag.disable();
|
|
78
|
+
}
|
|
79
|
+
function resetInstalledTracingAdapters(installedAdapters) {
|
|
80
|
+
if (installedAdapters.subSpan) {
|
|
81
|
+
(0, tracing_js_1.resetSubSpan)();
|
|
82
|
+
}
|
|
83
|
+
if (installedAdapters.tracing) {
|
|
84
|
+
(0, tracing_js_1.resetTracing)();
|
|
85
|
+
}
|
|
86
|
+
return { tracing: false, subSpan: false };
|
|
87
|
+
}
|
|
88
|
+
async function shutdownProviderWithTimeout(provider, shutdownTimeoutMs) {
|
|
89
|
+
let timeoutId;
|
|
90
|
+
let timedOut = false;
|
|
91
|
+
const shutdownPromise = provider.shutdown();
|
|
92
|
+
const observedShutdownPromise = shutdownPromise.catch((error) => {
|
|
93
|
+
if (!timedOut) {
|
|
94
|
+
api_js_1.log.warn({ error }, '[OpenTelemetry] provider.shutdown() failed');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
await Promise.race([
|
|
99
|
+
observedShutdownPromise,
|
|
100
|
+
new Promise((resolve) => {
|
|
101
|
+
timeoutId = setTimeout(() => {
|
|
102
|
+
timedOut = true;
|
|
103
|
+
// shutdownPromise rejection (if any) is handled by observedShutdownPromise above.
|
|
104
|
+
void shutdownPromise.catch(() => undefined);
|
|
105
|
+
api_js_1.log.warn('[OpenTelemetry] provider.shutdown() timed out after %dms; continuing worker shutdown', shutdownTimeoutMs);
|
|
106
|
+
resolve();
|
|
107
|
+
}, shutdownTimeoutMs);
|
|
108
|
+
}),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (timeoutId) {
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function init(opts = {}) {
|
|
118
|
+
if ((0, opentelemetryState_js_1.getOpenTelemetryTracerProvider)()) {
|
|
119
|
+
(0, api_js_1.message)('[OpenTelemetry] init() called more than once; ignoring duplicate call.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let installedAdapters = { tracing: false, subSpan: false };
|
|
123
|
+
let otelApi;
|
|
124
|
+
let registeredProvider;
|
|
125
|
+
let unregisterFastifyConfig;
|
|
126
|
+
let unregisterWorkerShutdownHook;
|
|
127
|
+
let ownsOpenTelemetryGlobals = false;
|
|
128
|
+
try {
|
|
129
|
+
/* eslint-disable @typescript-eslint/no-require-imports, global-require --
|
|
130
|
+
* Lazy require so init() can no-op when peer deps are missing instead of
|
|
131
|
+
* crashing at module load time. */
|
|
132
|
+
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
|
|
133
|
+
const { BatchSpanProcessor, SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
|
|
134
|
+
const { resourceFromAttributes } = require('@opentelemetry/resources');
|
|
135
|
+
const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
|
|
136
|
+
otelApi = require('@opentelemetry/api');
|
|
137
|
+
const loadedOtelApi = otelApi;
|
|
138
|
+
const resourceAttributes = {
|
|
139
|
+
...parseResourceAttributes(process.env.OTEL_RESOURCE_ATTRIBUTES),
|
|
140
|
+
...(opts.resourceAttributes ?? {}),
|
|
141
|
+
};
|
|
142
|
+
const configuredServiceName = resolveConfiguredServiceName(opts);
|
|
143
|
+
const serviceName = configuredServiceName ?? resourceAttributes[ATTR_SERVICE_NAME] ?? DEFAULT_SERVICE_NAME;
|
|
144
|
+
const shutdownTimeoutMs = resolveShutdownTimeoutMs(opts);
|
|
145
|
+
const resource = resourceFromAttributes({
|
|
146
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
147
|
+
...resourceAttributes,
|
|
148
|
+
...(configuredServiceName ? { [ATTR_SERVICE_NAME]: configuredServiceName } : {}),
|
|
149
|
+
});
|
|
150
|
+
const defaultExporter = () => {
|
|
151
|
+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
152
|
+
return new OTLPTraceExporter();
|
|
153
|
+
};
|
|
154
|
+
/* eslint-enable @typescript-eslint/no-require-imports, global-require */
|
|
155
|
+
const spanProcessor = opts.spanProcessor ??
|
|
156
|
+
(() => {
|
|
157
|
+
const exporter = opts.exporter ?? defaultExporter();
|
|
158
|
+
return isProduction() ? new BatchSpanProcessor(exporter) : new SimpleSpanProcessor(exporter);
|
|
159
|
+
})();
|
|
160
|
+
const provider = new NodeTracerProvider({
|
|
161
|
+
resource,
|
|
162
|
+
spanProcessors: [spanProcessor],
|
|
163
|
+
});
|
|
164
|
+
// Take ownership of the global tracer provider BEFORE installing module
|
|
165
|
+
// patches via registerInstrumentations(). provider.register() calls
|
|
166
|
+
// trace.setGlobalTracerProvider() internally, which silently fails (returns
|
|
167
|
+
// false but does not throw) when another OpenTelemetry SDK already owns the
|
|
168
|
+
// global proxy's delegate. Call setGlobalTracerProvider() directly first so
|
|
169
|
+
// we can detect that silent failure and bail before patching modules.
|
|
170
|
+
const acquiredTracerGlobal = loadedOtelApi.trace.setGlobalTracerProvider(provider);
|
|
171
|
+
if (!acquiredTracerGlobal) {
|
|
172
|
+
installedAdapters = resetInstalledTracingAdapters(installedAdapters);
|
|
173
|
+
void provider.shutdown().catch(() => undefined);
|
|
174
|
+
(0, api_js_1.message)('[OpenTelemetry] init: another OpenTelemetry tracer provider is already registered globally; aborting.');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Mark ownership BEFORE provider.register() so the outer catch's cleanup
|
|
178
|
+
// (which keys off ownsOpenTelemetryGlobals + the module-local provider
|
|
179
|
+
// reference) correctly disables the globals if register() throws.
|
|
180
|
+
ownsOpenTelemetryGlobals = true;
|
|
181
|
+
registeredProvider = provider;
|
|
182
|
+
(0, opentelemetryState_js_1.setOpenTelemetryTracerProvider)(provider);
|
|
183
|
+
// Re-call provider.register() to set context manager + propagator globals.
|
|
184
|
+
// The second setGlobalTracerProvider() call inside register() is a no-op
|
|
185
|
+
// (registerGlobal returns false because the proxy is already owned by us),
|
|
186
|
+
// but the propagator + context manager setup still runs.
|
|
187
|
+
provider.register();
|
|
188
|
+
configureOpenTelemetryDiagnostics(loadedOtelApi);
|
|
189
|
+
if (opts.fastify) {
|
|
190
|
+
/* eslint-disable @typescript-eslint/no-require-imports, global-require */
|
|
191
|
+
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
|
|
192
|
+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
193
|
+
// @fastify/otel uses `export = exported` so the require() returns the namespace
|
|
194
|
+
// object; the constructor lives on `.FastifyOtelInstrumentation` (also as `.default`).
|
|
195
|
+
const { FastifyOtelInstrumentation } = require('@fastify/otel');
|
|
196
|
+
/* eslint-enable @typescript-eslint/no-require-imports, global-require */
|
|
197
|
+
registerInstrumentations({
|
|
198
|
+
instrumentations: [
|
|
199
|
+
// HTTP first — Fastify instrumentation depends on it.
|
|
200
|
+
new HttpInstrumentation(),
|
|
201
|
+
new FastifyOtelInstrumentation({ registerOnInitialization: true }),
|
|
202
|
+
],
|
|
203
|
+
tracerProvider: provider,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (opts.tracing) {
|
|
207
|
+
const tracer = loadedOtelApi.trace.getTracer(serviceName);
|
|
208
|
+
installedAdapters.tracing = (0, tracing_js_1.setupTracing)({
|
|
209
|
+
startSsrRequestOptions: () => ({
|
|
210
|
+
// Keep the root span free of request payload data. Future safe
|
|
211
|
+
// attributes should be derived from structured metadata supplied by
|
|
212
|
+
// Ruby, not parsed out of the executable renderingRequest string.
|
|
213
|
+
opentelemetry: { name: 'ror.ssr.request' },
|
|
214
|
+
}),
|
|
215
|
+
executor: async (fn, unitOfWorkOptions) => {
|
|
216
|
+
const otelOpts = unitOfWorkOptions.opentelemetry ?? { name: 'ror.ssr.request' };
|
|
217
|
+
return tracer.startActiveSpan(otelOpts.name, { attributes: otelOpts.attributes }, async (span) => {
|
|
218
|
+
try {
|
|
219
|
+
return await fn();
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
span.setStatus({
|
|
223
|
+
code: loadedOtelApi.SpanStatusCode.ERROR,
|
|
224
|
+
message: err instanceof Error ? err.message : String(err),
|
|
225
|
+
});
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
span.end();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
if (installedAdapters.tracing) {
|
|
235
|
+
const subSpanImpl = (subOpts, fn) => tracer.startActiveSpan(subOpts.name, { attributes: subOpts.attributes }, async (span) => {
|
|
236
|
+
const controller = {
|
|
237
|
+
setAttributes(attributes) {
|
|
238
|
+
span.setAttributes(attributes);
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
try {
|
|
242
|
+
return await fn(controller);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
span.setStatus({
|
|
246
|
+
code: loadedOtelApi.SpanStatusCode.ERROR,
|
|
247
|
+
message: err instanceof Error ? err.message : String(err),
|
|
248
|
+
});
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
span.end();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
installedAdapters.subSpan = (0, tracing_js_1.setupSubSpan)(subSpanImpl);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
(0, api_js_1.message)('[OpenTelemetry] tracing integration was not installed because another tracing integration is ' +
|
|
259
|
+
'active; skipping OpenTelemetry sub-spans.');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let shutdownOpenTelemetryPromise;
|
|
263
|
+
const shutdownOpenTelemetry = () => {
|
|
264
|
+
shutdownOpenTelemetryPromise ?? (shutdownOpenTelemetryPromise = (async () => {
|
|
265
|
+
try {
|
|
266
|
+
await shutdownProviderWithTimeout(provider, shutdownTimeoutMs);
|
|
267
|
+
if ((0, opentelemetryState_js_1.getOpenTelemetryTracerProvider)() === provider) {
|
|
268
|
+
(0, opentelemetryState_js_1.setOpenTelemetryTracerProvider)(null);
|
|
269
|
+
disableOpenTelemetryGlobals(loadedOtelApi);
|
|
270
|
+
ownsOpenTelemetryGlobals = false;
|
|
271
|
+
installedAdapters = resetInstalledTracingAdapters(installedAdapters);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
unregisterFastifyConfig?.();
|
|
276
|
+
unregisterWorkerShutdownHook?.();
|
|
277
|
+
}
|
|
278
|
+
})());
|
|
279
|
+
return shutdownOpenTelemetryPromise;
|
|
280
|
+
};
|
|
281
|
+
// Register these last so failed init paths do not leave partial shutdown hooks
|
|
282
|
+
// behind. The worker hook runs during cluster restarts, while Fastify onClose
|
|
283
|
+
// still handles explicit app.close() calls from tests or custom integrations.
|
|
284
|
+
unregisterWorkerShutdownHook = (0, shutdownHooks_js_1.registerWorkerShutdownHook)(shutdownOpenTelemetry);
|
|
285
|
+
unregisterFastifyConfig = (0, fastifyConfig_js_1.registerFastifyConfigFunction)((app) => {
|
|
286
|
+
app.addHook('onClose', shutdownOpenTelemetry);
|
|
287
|
+
});
|
|
288
|
+
api_js_1.log.info('[OpenTelemetry] Tracer provider initialized');
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
unregisterFastifyConfig?.();
|
|
292
|
+
unregisterWorkerShutdownHook?.();
|
|
293
|
+
if (ownsOpenTelemetryGlobals &&
|
|
294
|
+
registeredProvider &&
|
|
295
|
+
otelApi &&
|
|
296
|
+
(0, opentelemetryState_js_1.getOpenTelemetryTracerProvider)() === registeredProvider) {
|
|
297
|
+
(0, opentelemetryState_js_1.setOpenTelemetryTracerProvider)(null);
|
|
298
|
+
disableOpenTelemetryGlobals(otelApi);
|
|
299
|
+
ownsOpenTelemetryGlobals = false;
|
|
300
|
+
}
|
|
301
|
+
installedAdapters = resetInstalledTracingAdapters(installedAdapters);
|
|
302
|
+
(0, api_js_1.message)(`[OpenTelemetry] init failed: ${String(err)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=opentelemetry.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NodeTracerProvider as NodeTracerProviderType } from '@opentelemetry/sdk-trace-node';
|
|
2
|
+
export declare function getOpenTelemetryTracerProvider(): NodeTracerProviderType | null;
|
|
3
|
+
export declare function setOpenTelemetryTracerProvider(provider: NodeTracerProviderType | null): void;
|
|
4
|
+
//# sourceMappingURL=opentelemetryState.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getOpenTelemetryTracerProvider = getOpenTelemetryTracerProvider;
|
|
4
|
+
exports.setOpenTelemetryTracerProvider = setOpenTelemetryTracerProvider;
|
|
5
|
+
let tracerProvider = null;
|
|
6
|
+
function getOpenTelemetryTracerProvider() {
|
|
7
|
+
return tracerProvider;
|
|
8
|
+
}
|
|
9
|
+
function setOpenTelemetryTracerProvider(provider) {
|
|
10
|
+
tracerProvider = provider;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=opentelemetryState.js.map
|
package/lib/shared/tracing.d.ts
CHANGED
|
@@ -43,11 +43,90 @@ export interface TracingIntegrationOptions {
|
|
|
43
43
|
* @param options.startSsrRequestOptions - Options used to start a new unit of work for an SSR request.
|
|
44
44
|
* Should be an object with your integration name as the only property.
|
|
45
45
|
* It will be passed to the executor.
|
|
46
|
+
* @returns true when this call installed the tracing integration.
|
|
46
47
|
*/
|
|
47
|
-
export declare function setupTracing(options: TracingIntegrationOptions):
|
|
48
|
+
export declare function setupTracing(options: TracingIntegrationOptions): boolean;
|
|
48
49
|
/**
|
|
49
50
|
* Reports a unit of work to the tracing service, if any.
|
|
50
51
|
*/
|
|
51
52
|
export declare function trace<T>(fn: UnitOfWork<T>, unitOfWorkOptions: UnitOfWorkOptions): Promise<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Resets the installed tracing executor + startSsrRequestOptions back to
|
|
55
|
+
* defaults. Internal integrations use this during lifecycle teardown.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resetTracing(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Test-only: reset the installed tracing executor + startSsrRequestOptions back
|
|
60
|
+
* to defaults. Not part of the public api — do not re-export from
|
|
61
|
+
* `integrations/api.ts`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function __resetTracingForTest(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Options passed to a sub-span wrapper.
|
|
66
|
+
*
|
|
67
|
+
* `name` is the span name (use dot.namespaced form, e.g. `ror.bundle.upload`).
|
|
68
|
+
* `attributes` are arbitrary key/value pairs attached to the span at creation.
|
|
69
|
+
*/
|
|
70
|
+
export interface SubSpanOptions {
|
|
71
|
+
name: string;
|
|
72
|
+
attributes?: Record<string, string | number | boolean>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Handed to the wrapped function so it can record attributes that are only
|
|
76
|
+
* known after the work runs (e.g., response byte counts). Implementations
|
|
77
|
+
* forward calls to the underlying span; the no-op default discards them.
|
|
78
|
+
*
|
|
79
|
+
* Only include byte counts, hashes, and counts here — never raw request or
|
|
80
|
+
* response payloads. Span attributes are not redacted by the renderer's
|
|
81
|
+
* logging policy.
|
|
82
|
+
*/
|
|
83
|
+
export interface SubSpanController {
|
|
84
|
+
setAttributes(attributes: Record<string, string | number | boolean>): void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Signature of a sub-span implementation installed via {@link setupSubSpan}.
|
|
88
|
+
* Must invoke `fn(controller)` and return its result. May wrap `fn` in a
|
|
89
|
+
* tracing span. The implementation supplies a controller that forwards
|
|
90
|
+
* `setAttributes` to the span; pass a no-op controller (e.g.
|
|
91
|
+
* `{ setAttributes() {} }`) when no span is being created.
|
|
92
|
+
*
|
|
93
|
+
* Implementations must either invoke `fn` synchronously or not invoke it at
|
|
94
|
+
* all before throwing/rejecting. Deferred invocation, such as scheduling `fn`
|
|
95
|
+
* with `setImmediate`, is unsupported because the fallback may run `fn`
|
|
96
|
+
* itself to preserve renderer behavior when an implementation fails before
|
|
97
|
+
* invocation.
|
|
98
|
+
*/
|
|
99
|
+
export type SubSpanFn = <T>(opts: SubSpanOptions, fn: (controller: SubSpanController) => Promise<T>) => Promise<T>;
|
|
100
|
+
/**
|
|
101
|
+
* Install a sub-span implementation. Integrations call this from their `init()`
|
|
102
|
+
* to start receiving sub-span events. If never called, sub-spans are no-ops.
|
|
103
|
+
* @returns true when this call installed the sub-span integration.
|
|
104
|
+
*/
|
|
105
|
+
export declare function setupSubSpan(impl: SubSpanFn): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Wrap an async function in a named sub-span. Safe to call even when no
|
|
108
|
+
* integration is installed — defaults to passing through to `fn`.
|
|
109
|
+
*
|
|
110
|
+
* The wrapped function receives a {@link SubSpanController} it can use to
|
|
111
|
+
* attach attributes that are only known after the work runs (e.g., response
|
|
112
|
+
* byte counts). With no integration installed, attribute updates are dropped.
|
|
113
|
+
*
|
|
114
|
+
* If the installed implementation throws or rejects before invoking `fn`, the
|
|
115
|
+
* caller is shielded: `fn` is still executed outside any sub-span (with the
|
|
116
|
+
* no-op controller) and its result returned. If the implementation fails
|
|
117
|
+
* after invoking `fn`, the error is rethrown so `fn` is never run twice.
|
|
118
|
+
*/
|
|
119
|
+
export declare function subSpan<T>(opts: SubSpanOptions, fn: (controller: SubSpanController) => Promise<T>): Promise<T>;
|
|
120
|
+
/**
|
|
121
|
+
* Resets the installed sub-span implementation back to the default pass-through.
|
|
122
|
+
* Internal integrations use this during lifecycle teardown.
|
|
123
|
+
*/
|
|
124
|
+
export declare function resetSubSpan(): void;
|
|
125
|
+
/**
|
|
126
|
+
* Test-only: reset the installed sub-span implementation back to the default
|
|
127
|
+
* pass-through. Not part of the public api — do not re-export from
|
|
128
|
+
* `integrations/api.ts`.
|
|
129
|
+
*/
|
|
130
|
+
export declare function __resetSubSpanForTest(): void;
|
|
52
131
|
export {};
|
|
53
132
|
//# sourceMappingURL=tracing.d.ts.map
|
package/lib/shared/tracing.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.startSsrRequestOptions = void 0;
|
|
4
7
|
exports.setupTracing = setupTracing;
|
|
5
8
|
exports.trace = trace;
|
|
9
|
+
exports.resetTracing = resetTracing;
|
|
10
|
+
exports.__resetTracingForTest = __resetTracingForTest;
|
|
11
|
+
exports.setupSubSpan = setupSubSpan;
|
|
12
|
+
exports.subSpan = subSpan;
|
|
13
|
+
exports.resetSubSpan = resetSubSpan;
|
|
14
|
+
exports.__resetSubSpanForTest = __resetSubSpanForTest;
|
|
15
|
+
const log_js_1 = __importDefault(require("./log.js"));
|
|
6
16
|
const errorReporter_js_1 = require("./errorReporter.js");
|
|
7
17
|
/* eslint-enable @typescript-eslint/no-empty-object-type */
|
|
8
18
|
let setupRun = false;
|
|
@@ -18,17 +28,19 @@ exports.startSsrRequestOptions = startSsrRequestOptions;
|
|
|
18
28
|
* @param options.startSsrRequestOptions - Options used to start a new unit of work for an SSR request.
|
|
19
29
|
* Should be an object with your integration name as the only property.
|
|
20
30
|
* It will be passed to the executor.
|
|
31
|
+
* @returns true when this call installed the tracing integration.
|
|
21
32
|
*/
|
|
22
33
|
function setupTracing(options) {
|
|
23
34
|
if (setupRun) {
|
|
24
35
|
(0, errorReporter_js_1.message)('setupTracing called more than once. Currently only one tracing integration can be enabled.');
|
|
25
|
-
return;
|
|
36
|
+
return false;
|
|
26
37
|
}
|
|
27
38
|
executor = options.executor;
|
|
28
39
|
if (options.startSsrRequestOptions) {
|
|
29
40
|
mutableStartSsrRequestOptions = options.startSsrRequestOptions;
|
|
30
41
|
}
|
|
31
42
|
setupRun = true;
|
|
43
|
+
return true;
|
|
32
44
|
}
|
|
33
45
|
/**
|
|
34
46
|
* Reports a unit of work to the tracing service, if any.
|
|
@@ -36,4 +48,100 @@ function setupTracing(options) {
|
|
|
36
48
|
function trace(fn, unitOfWorkOptions) {
|
|
37
49
|
return executor(fn, unitOfWorkOptions);
|
|
38
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Resets the installed tracing executor + startSsrRequestOptions back to
|
|
53
|
+
* defaults. Internal integrations use this during lifecycle teardown.
|
|
54
|
+
*/
|
|
55
|
+
function resetTracing() {
|
|
56
|
+
executor = (fn) => fn();
|
|
57
|
+
mutableStartSsrRequestOptions = () => ({});
|
|
58
|
+
setupRun = false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Test-only: reset the installed tracing executor + startSsrRequestOptions back
|
|
62
|
+
* to defaults. Not part of the public api — do not re-export from
|
|
63
|
+
* `integrations/api.ts`.
|
|
64
|
+
*/
|
|
65
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
66
|
+
function __resetTracingForTest() {
|
|
67
|
+
resetTracing();
|
|
68
|
+
}
|
|
69
|
+
const noOpSubSpanController = {
|
|
70
|
+
setAttributes() { },
|
|
71
|
+
};
|
|
72
|
+
const defaultSubSpan = (_opts, fn) => fn(noOpSubSpanController);
|
|
73
|
+
let subSpanImpl = defaultSubSpan;
|
|
74
|
+
let subSpanSetupRun = false;
|
|
75
|
+
/**
|
|
76
|
+
* Install a sub-span implementation. Integrations call this from their `init()`
|
|
77
|
+
* to start receiving sub-span events. If never called, sub-spans are no-ops.
|
|
78
|
+
* @returns true when this call installed the sub-span integration.
|
|
79
|
+
*/
|
|
80
|
+
function setupSubSpan(impl) {
|
|
81
|
+
if (subSpanSetupRun) {
|
|
82
|
+
(0, errorReporter_js_1.message)('setupSubSpan called more than once. Only one sub-span integration can be enabled.');
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
subSpanImpl = impl;
|
|
86
|
+
subSpanSetupRun = true;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Wrap an async function in a named sub-span. Safe to call even when no
|
|
91
|
+
* integration is installed — defaults to passing through to `fn`.
|
|
92
|
+
*
|
|
93
|
+
* The wrapped function receives a {@link SubSpanController} it can use to
|
|
94
|
+
* attach attributes that are only known after the work runs (e.g., response
|
|
95
|
+
* byte counts). With no integration installed, attribute updates are dropped.
|
|
96
|
+
*
|
|
97
|
+
* If the installed implementation throws or rejects before invoking `fn`, the
|
|
98
|
+
* caller is shielded: `fn` is still executed outside any sub-span (with the
|
|
99
|
+
* no-op controller) and its result returned. If the implementation fails
|
|
100
|
+
* after invoking `fn`, the error is rethrown so `fn` is never run twice.
|
|
101
|
+
*/
|
|
102
|
+
function subSpan(opts, fn) {
|
|
103
|
+
let invoked = false;
|
|
104
|
+
const wrappedFn = (controller) => {
|
|
105
|
+
invoked = true;
|
|
106
|
+
return fn(controller);
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
return Promise.resolve(subSpanImpl(opts, wrappedFn)).catch((err) => {
|
|
110
|
+
if (invoked) {
|
|
111
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
112
|
+
}
|
|
113
|
+
// log.warn (not message) for the silent-fallback path. message() goes to
|
|
114
|
+
// log.error + external notifiers (Sentry/Bugsnag/etc.) on every request,
|
|
115
|
+
// which is too noisy for a per-request recoverable failure where fn() is
|
|
116
|
+
// still executed. log.warn surfaces the broken integration without
|
|
117
|
+
// paging the on-call team for every render.
|
|
118
|
+
log_js_1.default.warn({ err }, 'subSpan implementation rejected before invoking fn(); running fn() without a span');
|
|
119
|
+
return wrappedFn(noOpSubSpanController);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (invoked) {
|
|
124
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
125
|
+
}
|
|
126
|
+
log_js_1.default.warn({ err }, 'subSpan implementation threw before invoking fn(); running fn() without a span');
|
|
127
|
+
return wrappedFn(noOpSubSpanController);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resets the installed sub-span implementation back to the default pass-through.
|
|
132
|
+
* Internal integrations use this during lifecycle teardown.
|
|
133
|
+
*/
|
|
134
|
+
function resetSubSpan() {
|
|
135
|
+
subSpanImpl = defaultSubSpan;
|
|
136
|
+
subSpanSetupRun = false;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Test-only: reset the installed sub-span implementation back to the default
|
|
140
|
+
* pass-through. Not part of the public api — do not re-export from
|
|
141
|
+
* `integrations/api.ts`.
|
|
142
|
+
*/
|
|
143
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
144
|
+
function __resetSubSpanForTest() {
|
|
145
|
+
resetSubSpan();
|
|
146
|
+
}
|
|
39
147
|
//# sourceMappingURL=tracing.js.map
|