@pingops/otel 0.1.0 → 0.1.2
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/index.cjs +1018 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +342 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +342 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +981 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +23 -11
- package/dist/config.d.ts +0 -75
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -9
- package/dist/index.js.map +0 -1
- package/dist/instrumentations/body-extractor.d.ts +0 -48
- package/dist/instrumentations/body-extractor.d.ts.map +0 -1
- package/dist/instrumentations/body-extractor.js +0 -361
- package/dist/instrumentations/body-extractor.js.map +0 -1
- package/dist/instrumentations/http.d.ts +0 -12
- package/dist/instrumentations/http.d.ts.map +0 -1
- package/dist/instrumentations/http.js +0 -38
- package/dist/instrumentations/http.js.map +0 -1
- package/dist/instrumentations/index.d.ts +0 -17
- package/dist/instrumentations/index.d.ts.map +0 -1
- package/dist/instrumentations/index.js +0 -32
- package/dist/instrumentations/index.js.map +0 -1
- package/dist/instrumentations/undici.d.ts +0 -12
- package/dist/instrumentations/undici.d.ts.map +0 -1
- package/dist/instrumentations/undici.js +0 -38
- package/dist/instrumentations/undici.js.map +0 -1
- package/dist/processor.d.ts +0 -82
- package/dist/processor.d.ts.map +0 -1
- package/dist/processor.js +0 -264
- package/dist/processor.js.map +0 -1
- package/dist/span-processor.d.ts +0 -78
- package/dist/span-processor.d.ts.map +0 -1
- package/dist/span-processor.js +0 -272
- package/dist/span-processor.js.map +0 -1
- package/dist/span-wrapper.d.ts +0 -60
- package/dist/span-wrapper.d.ts.map +0 -1
- package/dist/span-wrapper.js +0 -118
- package/dist/span-wrapper.js.map +0 -1
- package/dist/tracer-provider.d.ts +0 -57
- package/dist/tracer-provider.d.ts.map +0 -1
- package/dist/tracer-provider.js +0 -182
- package/dist/tracer-provider.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
let _opentelemetry_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
29
|
+
let _opentelemetry_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
|
|
30
|
+
let _pingops_core = require("@pingops/core");
|
|
31
|
+
let _opentelemetry_api = require("@opentelemetry/api");
|
|
32
|
+
require("@opentelemetry/sdk-trace-node");
|
|
33
|
+
require("@opentelemetry/resources");
|
|
34
|
+
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
35
|
+
let http = require("http");
|
|
36
|
+
let _opentelemetry_instrumentation_http = require("@opentelemetry/instrumentation-http");
|
|
37
|
+
let diagnostics_channel = require("diagnostics_channel");
|
|
38
|
+
diagnostics_channel = __toESM(diagnostics_channel);
|
|
39
|
+
let url = require("url");
|
|
40
|
+
let _opentelemetry_instrumentation = require("@opentelemetry/instrumentation");
|
|
41
|
+
let _opentelemetry_core = require("@opentelemetry/core");
|
|
42
|
+
|
|
43
|
+
//#region src/config-store.ts
|
|
44
|
+
let globalConfig = null;
|
|
45
|
+
/**
|
|
46
|
+
* Sets the global processor configuration
|
|
47
|
+
* @param config - Configuration to store
|
|
48
|
+
*/
|
|
49
|
+
function setGlobalConfig(config) {
|
|
50
|
+
globalConfig = config;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Gets the global processor configuration
|
|
54
|
+
* @returns The stored configuration or null if not set
|
|
55
|
+
*/
|
|
56
|
+
function getGlobalConfig() {
|
|
57
|
+
return globalConfig;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/span-processor.ts
|
|
62
|
+
const logger$1 = (0, _pingops_core.createLogger)("[PingOps Processor]");
|
|
63
|
+
/**
|
|
64
|
+
* Creates a filtered span wrapper that applies header filtering to attributes
|
|
65
|
+
*
|
|
66
|
+
* This wrapper applies both domain-specific and global header filtering:
|
|
67
|
+
* - Uses domain allow list to determine domain-specific header rules
|
|
68
|
+
* - Applies global header allow/deny lists
|
|
69
|
+
* - Filters headers from http.request.header and http.response.header attributes
|
|
70
|
+
*
|
|
71
|
+
* Uses a Proxy to automatically forward all properties and methods to the original span,
|
|
72
|
+
* except for 'attributes' which returns the filtered version. This approach is future-proof
|
|
73
|
+
* and will work with any new methods or properties added to ReadableSpan.
|
|
74
|
+
*
|
|
75
|
+
* This allows us to filter headers before the span is serialized by OTLP exporter
|
|
76
|
+
*/
|
|
77
|
+
function createFilteredSpan(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction) {
|
|
78
|
+
const payload = (0, _pingops_core.extractSpanPayload)(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction);
|
|
79
|
+
const filteredAttributes = payload?.attributes ?? span.attributes;
|
|
80
|
+
logger$1.debug("Payload", { payload });
|
|
81
|
+
return new Proxy(span, { get(target, prop) {
|
|
82
|
+
if (prop === "attributes") return filteredAttributes;
|
|
83
|
+
const value = target[prop];
|
|
84
|
+
if (typeof value === "function") return value.bind(target);
|
|
85
|
+
return value;
|
|
86
|
+
} });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* OpenTelemetry span processor for sending spans to PingOps backend.
|
|
90
|
+
*
|
|
91
|
+
* This processor wraps OpenTelemetry's built-in processors (BatchSpanProcessor or SimpleSpanProcessor)
|
|
92
|
+
* and applies filtering before passing spans to the OTLP exporter.
|
|
93
|
+
*/
|
|
94
|
+
var PingopsSpanProcessor = class {
|
|
95
|
+
processor;
|
|
96
|
+
config;
|
|
97
|
+
/**
|
|
98
|
+
* Creates a new PingopsSpanProcessor instance.
|
|
99
|
+
*
|
|
100
|
+
* @param config - Configuration parameters for the processor
|
|
101
|
+
*/
|
|
102
|
+
constructor(config) {
|
|
103
|
+
const exportMode = config.exportMode ?? "batched";
|
|
104
|
+
const apiKey = config.apiKey || process.env.PINGOPS_API_KEY || "";
|
|
105
|
+
const exporter = new _opentelemetry_exporter_trace_otlp_http.OTLPTraceExporter({
|
|
106
|
+
url: `${config.baseUrl}/v1/traces`,
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: apiKey ? `Bearer ${apiKey}` : "",
|
|
109
|
+
"Content-Type": "application/json"
|
|
110
|
+
},
|
|
111
|
+
timeoutMillis: 5e3
|
|
112
|
+
});
|
|
113
|
+
if (exportMode === "immediate") this.processor = new _opentelemetry_sdk_trace_base.SimpleSpanProcessor(exporter);
|
|
114
|
+
else this.processor = new _opentelemetry_sdk_trace_base.BatchSpanProcessor(exporter, {
|
|
115
|
+
maxExportBatchSize: config.batchSize ?? 50,
|
|
116
|
+
scheduledDelayMillis: config.batchTimeout ?? 5e3
|
|
117
|
+
});
|
|
118
|
+
this.config = {
|
|
119
|
+
debug: config.debug ?? false,
|
|
120
|
+
headersAllowList: config.headersAllowList,
|
|
121
|
+
headersDenyList: config.headersDenyList,
|
|
122
|
+
domainAllowList: config.domainAllowList,
|
|
123
|
+
domainDenyList: config.domainDenyList,
|
|
124
|
+
captureRequestBody: config.captureRequestBody,
|
|
125
|
+
captureResponseBody: config.captureResponseBody,
|
|
126
|
+
headerRedaction: config.headerRedaction
|
|
127
|
+
};
|
|
128
|
+
setGlobalConfig({
|
|
129
|
+
captureRequestBody: config.captureRequestBody,
|
|
130
|
+
captureResponseBody: config.captureResponseBody,
|
|
131
|
+
domainAllowList: config.domainAllowList
|
|
132
|
+
});
|
|
133
|
+
logger$1.info("Initialized PingopsSpanProcessor", {
|
|
134
|
+
baseUrl: config.baseUrl,
|
|
135
|
+
exportMode,
|
|
136
|
+
batchSize: config.batchSize,
|
|
137
|
+
batchTimeout: config.batchTimeout,
|
|
138
|
+
hasDomainAllowList: !!config.domainAllowList && config.domainAllowList.length > 0,
|
|
139
|
+
hasDomainDenyList: !!config.domainDenyList && config.domainDenyList.length > 0,
|
|
140
|
+
hasHeadersAllowList: !!config.headersAllowList && config.headersAllowList.length > 0,
|
|
141
|
+
hasHeadersDenyList: !!config.headersDenyList && config.headersDenyList.length > 0
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Called when a span starts - extracts parent attributes from context and adds them to the span
|
|
146
|
+
*/
|
|
147
|
+
onStart(span, parentContext) {
|
|
148
|
+
const spanContext = span.spanContext();
|
|
149
|
+
logger$1.debug("Span started", {
|
|
150
|
+
spanName: span.name,
|
|
151
|
+
spanId: spanContext.spanId,
|
|
152
|
+
traceId: spanContext.traceId
|
|
153
|
+
});
|
|
154
|
+
const propagatedAttributes = (0, _pingops_core.getPropagatedAttributesFromContext)(parentContext);
|
|
155
|
+
if (Object.keys(propagatedAttributes).length > 0) {
|
|
156
|
+
for (const [key, value] of Object.entries(propagatedAttributes)) if (typeof value === "string" || Array.isArray(value)) span.setAttribute(key, value);
|
|
157
|
+
logger$1.debug("Set propagated attributes on span", {
|
|
158
|
+
spanName: span.name,
|
|
159
|
+
attributeKeys: Object.keys(propagatedAttributes)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
this.processor.onStart(span, parentContext);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Called when a span ends. Filters the span and passes it to the underlying processor if eligible.
|
|
166
|
+
*
|
|
167
|
+
* This method:
|
|
168
|
+
* 1. Checks if the span is eligible (CLIENT + HTTP/GenAI attributes)
|
|
169
|
+
* 2. Applies domain filtering (determines if span should be exported)
|
|
170
|
+
* 3. Applies header filtering via FilteredSpan wrapper (domain-specific and global rules)
|
|
171
|
+
* 4. If eligible, passes filtered span to underlying OTLP processor for export
|
|
172
|
+
*/
|
|
173
|
+
onEnd(span) {
|
|
174
|
+
const spanContext = span.spanContext();
|
|
175
|
+
logger$1.debug("Span ended, processing", {
|
|
176
|
+
spanName: span.name,
|
|
177
|
+
spanId: spanContext.spanId,
|
|
178
|
+
traceId: spanContext.traceId,
|
|
179
|
+
spanKind: span.kind
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
if (!(0, _pingops_core.isSpanEligible)(span)) {
|
|
183
|
+
logger$1.debug("Span not eligible, skipping", {
|
|
184
|
+
spanName: span.name,
|
|
185
|
+
spanId: spanContext.spanId,
|
|
186
|
+
reason: "not CLIENT or missing HTTP/GenAI attributes"
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const attributes = span.attributes;
|
|
191
|
+
const url$1 = attributes["http.url"] || attributes["url.full"] || (attributes["server.address"] ? `https://${String(attributes["server.address"])}` : "");
|
|
192
|
+
logger$1.debug("Extracted URL for domain filtering", {
|
|
193
|
+
spanName: span.name,
|
|
194
|
+
url: url$1,
|
|
195
|
+
hasHttpUrl: !!attributes["http.url"],
|
|
196
|
+
hasUrlFull: !!attributes["url.full"],
|
|
197
|
+
hasServerAddress: !!attributes["server.address"]
|
|
198
|
+
});
|
|
199
|
+
if (url$1) {
|
|
200
|
+
if (!(0, _pingops_core.shouldCaptureSpan)(url$1, this.config.domainAllowList, this.config.domainDenyList)) {
|
|
201
|
+
logger$1.info("Span filtered out by domain rules", {
|
|
202
|
+
spanName: span.name,
|
|
203
|
+
spanId: spanContext.spanId,
|
|
204
|
+
url: url$1
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
} else logger$1.debug("No URL found for domain filtering, proceeding", { spanName: span.name });
|
|
209
|
+
const filteredSpan = createFilteredSpan(span, this.config.domainAllowList, this.config.headersAllowList, this.config.headersDenyList, this.config.captureRequestBody, this.config.captureResponseBody, this.config.headerRedaction);
|
|
210
|
+
this.processor.onEnd(filteredSpan);
|
|
211
|
+
logger$1.info("Span passed all filters and queued for export", {
|
|
212
|
+
spanName: span.name,
|
|
213
|
+
spanId: spanContext.spanId,
|
|
214
|
+
traceId: spanContext.traceId,
|
|
215
|
+
url: url$1,
|
|
216
|
+
hasHeaderFiltering: !!(this.config.headersAllowList || this.config.headersDenyList)
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger$1.error("Error processing span", {
|
|
220
|
+
spanName: span.name,
|
|
221
|
+
spanId: spanContext.spanId,
|
|
222
|
+
error: error instanceof Error ? error.message : String(error)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Forces an immediate flush of all pending spans.
|
|
228
|
+
*
|
|
229
|
+
* @returns Promise that resolves when all pending operations are complete
|
|
230
|
+
*/
|
|
231
|
+
async forceFlush() {
|
|
232
|
+
logger$1.info("Force flushing spans");
|
|
233
|
+
try {
|
|
234
|
+
await this.processor.forceFlush();
|
|
235
|
+
logger$1.info("Force flush complete");
|
|
236
|
+
} catch (error) {
|
|
237
|
+
logger$1.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Gracefully shuts down the processor, ensuring all pending operations are completed.
|
|
243
|
+
*
|
|
244
|
+
* @returns Promise that resolves when shutdown is complete
|
|
245
|
+
*/
|
|
246
|
+
async shutdown() {
|
|
247
|
+
logger$1.info("Shutting down processor");
|
|
248
|
+
try {
|
|
249
|
+
await this.processor.shutdown();
|
|
250
|
+
logger$1.info("Processor shutdown complete");
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger$1.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/tracer-provider.ts
|
|
260
|
+
/**
|
|
261
|
+
* Global symbol for PingOps state
|
|
262
|
+
*/
|
|
263
|
+
const PINGOPS_GLOBAL_SYMBOL = Symbol.for("pingops");
|
|
264
|
+
/**
|
|
265
|
+
* Logger instance for tracer provider
|
|
266
|
+
*/
|
|
267
|
+
const logger = (0, _pingops_core.createLogger)("[PingOps TracerProvider]");
|
|
268
|
+
/**
|
|
269
|
+
* Creates initial global state
|
|
270
|
+
*/
|
|
271
|
+
function createState() {
|
|
272
|
+
return { isolatedTracerProvider: null };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Gets the global state, creating it if it doesn't exist
|
|
276
|
+
*/
|
|
277
|
+
function getGlobalState() {
|
|
278
|
+
const initialState = createState();
|
|
279
|
+
try {
|
|
280
|
+
const g = globalThis;
|
|
281
|
+
if (typeof g !== "object" || g === null) {
|
|
282
|
+
logger.warn("globalThis is not available, using fallback state");
|
|
283
|
+
return initialState;
|
|
284
|
+
}
|
|
285
|
+
if (!g[PINGOPS_GLOBAL_SYMBOL]) {
|
|
286
|
+
logger.debug("Creating new global state");
|
|
287
|
+
Object.defineProperty(g, PINGOPS_GLOBAL_SYMBOL, {
|
|
288
|
+
value: initialState,
|
|
289
|
+
writable: false,
|
|
290
|
+
configurable: false,
|
|
291
|
+
enumerable: false
|
|
292
|
+
});
|
|
293
|
+
} else logger.debug("Retrieved existing global state");
|
|
294
|
+
return g[PINGOPS_GLOBAL_SYMBOL];
|
|
295
|
+
} catch (err) {
|
|
296
|
+
logger.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
|
|
297
|
+
return initialState;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Sets an isolated TracerProvider for PingOps tracing operations.
|
|
302
|
+
*
|
|
303
|
+
* This allows PingOps to use its own TracerProvider instance, separate from
|
|
304
|
+
* the global OpenTelemetry TracerProvider. This is useful for avoiding conflicts
|
|
305
|
+
* with other OpenTelemetry instrumentation in the application.
|
|
306
|
+
*
|
|
307
|
+
* @param provider - The TracerProvider instance to use, or null to clear the isolated provider
|
|
308
|
+
* @public
|
|
309
|
+
*/
|
|
310
|
+
function setPingopsTracerProvider(provider) {
|
|
311
|
+
const state = getGlobalState();
|
|
312
|
+
const hadProvider = state.isolatedTracerProvider !== null;
|
|
313
|
+
state.isolatedTracerProvider = provider;
|
|
314
|
+
if (provider) logger.info("Set isolated TracerProvider", {
|
|
315
|
+
hadPrevious: hadProvider,
|
|
316
|
+
providerType: provider.constructor.name
|
|
317
|
+
});
|
|
318
|
+
else logger.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Gets the TracerProvider for PingOps tracing operations.
|
|
322
|
+
*
|
|
323
|
+
* Returns the isolated TracerProvider if one has been set via setPingopsTracerProvider(),
|
|
324
|
+
* otherwise falls back to the global OpenTelemetry TracerProvider.
|
|
325
|
+
*
|
|
326
|
+
* @returns The TracerProvider instance to use for PingOps tracing
|
|
327
|
+
* @public
|
|
328
|
+
*/
|
|
329
|
+
function getPingopsTracerProvider() {
|
|
330
|
+
const { isolatedTracerProvider } = getGlobalState();
|
|
331
|
+
if (isolatedTracerProvider) {
|
|
332
|
+
logger.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
|
|
333
|
+
return isolatedTracerProvider;
|
|
334
|
+
}
|
|
335
|
+
const globalProvider = _opentelemetry_api.trace.getTracerProvider();
|
|
336
|
+
logger.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
|
|
337
|
+
return globalProvider;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Shuts down the TracerProvider and flushes remaining spans
|
|
341
|
+
*/
|
|
342
|
+
async function shutdownTracerProvider() {
|
|
343
|
+
logger.info("Shutting down TracerProvider");
|
|
344
|
+
const providerWithShutdown = getPingopsTracerProvider();
|
|
345
|
+
if (providerWithShutdown && typeof providerWithShutdown.shutdown === "function") {
|
|
346
|
+
logger.debug("Calling provider.shutdown()");
|
|
347
|
+
try {
|
|
348
|
+
await providerWithShutdown.shutdown();
|
|
349
|
+
logger.info("TracerProvider shutdown complete");
|
|
350
|
+
} catch (error) {
|
|
351
|
+
logger.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
} else logger.warn("TracerProvider does not have shutdown method, skipping");
|
|
355
|
+
setPingopsTracerProvider(null);
|
|
356
|
+
logger.info("TracerProvider shutdown finished");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/instrumentations/http/pingops-http.ts
|
|
361
|
+
/**
|
|
362
|
+
* Pingops HTTP instrumentation that extends HttpInstrumentation
|
|
363
|
+
* with request/response body capture and network timing metrics
|
|
364
|
+
*/
|
|
365
|
+
const DEFAULT_MAX_REQUEST_BODY_SIZE$1 = 4 * 1024;
|
|
366
|
+
const DEFAULT_MAX_RESPONSE_BODY_SIZE$1 = 4 * 1024;
|
|
367
|
+
const NETWORK_TIMINGS_PROP_NAME = "__networkTimings";
|
|
368
|
+
const PingopsSemanticAttributes = {
|
|
369
|
+
HTTP_REQUEST_BODY: "http.request.body",
|
|
370
|
+
HTTP_RESPONSE_BODY: "http.response.body",
|
|
371
|
+
NETWORK_DNS_LOOKUP_DURATION: "net.dns.lookup.duration",
|
|
372
|
+
NETWORK_TCP_CONNECT_DURATION: "net.tcp.connect.duration",
|
|
373
|
+
NETWORK_TLS_HANDSHAKE_DURATION: "net.tls.handshake.duration",
|
|
374
|
+
NETWORK_TTFB_DURATION: "net.ttfb.duration",
|
|
375
|
+
NETWORK_CONTENT_TRANSFER_DURATION: "net.content.transfer.duration"
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Manually flattens a nested object into dot-notation keys
|
|
379
|
+
*/
|
|
380
|
+
function flatten(obj, prefix = "") {
|
|
381
|
+
const result = {};
|
|
382
|
+
for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
383
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
384
|
+
const value = obj[key];
|
|
385
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Buffer)) Object.assign(result, flatten(value, newKey));
|
|
386
|
+
else result[newKey] = value;
|
|
387
|
+
}
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Sets an attribute value on a span, handling various types appropriately
|
|
392
|
+
*/
|
|
393
|
+
function setAttributeValue(span, attrName, attrValue) {
|
|
394
|
+
if (typeof attrValue === "string" || typeof attrValue === "number" || typeof attrValue === "boolean") span.setAttribute(attrName, attrValue);
|
|
395
|
+
else if (attrValue instanceof Buffer) span.setAttribute(attrName, attrValue.toString("utf8"));
|
|
396
|
+
else if (typeof attrValue == "object") span.setAttributes(flatten({ [attrName]: attrValue }));
|
|
397
|
+
else if (Array.isArray(attrValue)) if (attrValue.length) {
|
|
398
|
+
const firstElement = attrValue[0];
|
|
399
|
+
if (typeof firstElement === "string" || typeof firstElement === "number" || typeof firstElement === "boolean") span.setAttribute(attrName, attrValue);
|
|
400
|
+
} else span.setAttribute(attrName, attrValue);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Processes network timings and sets them as span attributes (no spans created)
|
|
404
|
+
*/
|
|
405
|
+
function processNetworkTimings(span, networkTimings) {
|
|
406
|
+
if (networkTimings.startAt && networkTimings.dnsLookupAt) span.setAttribute(PingopsSemanticAttributes.NETWORK_DNS_LOOKUP_DURATION, networkTimings.dnsLookupAt - networkTimings.startAt);
|
|
407
|
+
if (networkTimings.dnsLookupAt && networkTimings.tcpConnectionAt) span.setAttribute(PingopsSemanticAttributes.NETWORK_TCP_CONNECT_DURATION, networkTimings.tcpConnectionAt - networkTimings.dnsLookupAt);
|
|
408
|
+
if (networkTimings.tcpConnectionAt && networkTimings.tlsHandshakeAt) span.setAttribute(PingopsSemanticAttributes.NETWORK_TLS_HANDSHAKE_DURATION, networkTimings.tlsHandshakeAt - networkTimings.tcpConnectionAt);
|
|
409
|
+
const startTTFB = networkTimings.tlsHandshakeAt || networkTimings.tcpConnectionAt;
|
|
410
|
+
if (networkTimings.firstByteAt && startTTFB) span.setAttribute(PingopsSemanticAttributes.NETWORK_TTFB_DURATION, networkTimings.firstByteAt - startTTFB);
|
|
411
|
+
if (networkTimings.firstByteAt && networkTimings.endAt) span.setAttribute(PingopsSemanticAttributes.NETWORK_CONTENT_TRANSFER_DURATION, networkTimings.endAt - networkTimings.firstByteAt);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Initializes network timings on a span
|
|
415
|
+
*/
|
|
416
|
+
function initializeNetworkTimings(span) {
|
|
417
|
+
const networkTimings = { startAt: Date.now() };
|
|
418
|
+
Object.defineProperty(span, NETWORK_TIMINGS_PROP_NAME, {
|
|
419
|
+
enumerable: false,
|
|
420
|
+
configurable: true,
|
|
421
|
+
writable: false,
|
|
422
|
+
value: networkTimings
|
|
423
|
+
});
|
|
424
|
+
return networkTimings;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Extracts domain from URL
|
|
428
|
+
*/
|
|
429
|
+
function extractDomainFromUrl$1(url$1) {
|
|
430
|
+
try {
|
|
431
|
+
return new URL(url$1).hostname;
|
|
432
|
+
} catch {
|
|
433
|
+
const match = url$1.match(/^(?:https?:\/\/)?([^/]+)/);
|
|
434
|
+
return match ? match[1] : "";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Gets domain rule configuration for a given URL
|
|
439
|
+
*/
|
|
440
|
+
function getDomainRule$1(url$1, domainAllowList) {
|
|
441
|
+
if (!domainAllowList) return;
|
|
442
|
+
const domain = extractDomainFromUrl$1(url$1);
|
|
443
|
+
for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Determines if request body should be captured based on priority:
|
|
447
|
+
* context > domain rule > global config > default (false)
|
|
448
|
+
*/
|
|
449
|
+
function shouldCaptureRequestBody$1(url$1) {
|
|
450
|
+
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_REQUEST_BODY);
|
|
451
|
+
if (contextValue !== void 0) return contextValue;
|
|
452
|
+
if (url$1) {
|
|
453
|
+
const domainRule = getDomainRule$1(url$1, getGlobalConfig()?.domainAllowList);
|
|
454
|
+
if (domainRule?.captureRequestBody !== void 0) return domainRule.captureRequestBody;
|
|
455
|
+
}
|
|
456
|
+
const globalConfig$1 = getGlobalConfig();
|
|
457
|
+
if (globalConfig$1?.captureRequestBody !== void 0) return globalConfig$1.captureRequestBody;
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Determines if response body should be captured based on priority:
|
|
462
|
+
* context > domain rule > global config > default (false)
|
|
463
|
+
*/
|
|
464
|
+
function shouldCaptureResponseBody$1(url$1) {
|
|
465
|
+
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_RESPONSE_BODY);
|
|
466
|
+
if (contextValue !== void 0) return contextValue;
|
|
467
|
+
if (url$1) {
|
|
468
|
+
const domainRule = getDomainRule$1(url$1, getGlobalConfig()?.domainAllowList);
|
|
469
|
+
if (domainRule?.captureResponseBody !== void 0) return domainRule.captureResponseBody;
|
|
470
|
+
}
|
|
471
|
+
const globalConfig$1 = getGlobalConfig();
|
|
472
|
+
if (globalConfig$1?.captureResponseBody !== void 0) return globalConfig$1.captureResponseBody;
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Captures request body from string or Buffer data
|
|
477
|
+
*/
|
|
478
|
+
function captureRequestBody(span, data, maxSize, semanticAttr, url$1) {
|
|
479
|
+
if (!shouldCaptureRequestBody$1(url$1)) return;
|
|
480
|
+
if (data.length && data.length <= maxSize) try {
|
|
481
|
+
const requestBody = typeof data === "string" ? data : data.toString("utf-8");
|
|
482
|
+
if (requestBody) setAttributeValue(span, semanticAttr, requestBody);
|
|
483
|
+
} catch (e) {
|
|
484
|
+
console.error("Error occurred while capturing request body:", e);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Captures response body from chunks
|
|
489
|
+
*/
|
|
490
|
+
function captureResponseBody(span, chunks, semanticAttr, url$1) {
|
|
491
|
+
if (!shouldCaptureResponseBody$1(url$1)) return;
|
|
492
|
+
if (chunks && chunks.length) try {
|
|
493
|
+
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
494
|
+
if (responseBody) setAttributeValue(span, semanticAttr, responseBody);
|
|
495
|
+
} catch (e) {
|
|
496
|
+
console.error("Error occurred while capturing response body:", e);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Captures HTTP request headers as span attributes
|
|
501
|
+
*/
|
|
502
|
+
function captureRequestHeaders(span, headers) {
|
|
503
|
+
for (const [key, value] of Object.entries(headers)) if (value !== void 0) span.setAttribute(`pingops.http.request.header.${key.toLowerCase()}`, Array.isArray(value) ? value.join(",") : String(value));
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Captures HTTP response headers as span attributes
|
|
507
|
+
*/
|
|
508
|
+
function captureResponseHeaders(span, headers) {
|
|
509
|
+
for (const [key, value] of Object.entries(headers)) if (value !== void 0) span.setAttribute(`pingops.http.response.header.${key.toLowerCase()}`, Array.isArray(value) ? value.join(",") : String(value));
|
|
510
|
+
}
|
|
511
|
+
const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
|
|
512
|
+
var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_http.HttpInstrumentation {
|
|
513
|
+
constructor(config) {
|
|
514
|
+
super(config);
|
|
515
|
+
this._config = this._createConfig(config);
|
|
516
|
+
}
|
|
517
|
+
_createConfig(config) {
|
|
518
|
+
return {
|
|
519
|
+
...config,
|
|
520
|
+
requestHook: this._createRequestHook(config?.requestHook, config),
|
|
521
|
+
responseHook: this._createResponseHook(config?.responseHook, config)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
_createRequestHook(originalRequestHook, config) {
|
|
525
|
+
return (span, request) => {
|
|
526
|
+
const headers = request.headers;
|
|
527
|
+
if (headers) captureRequestHeaders(span, headers);
|
|
528
|
+
if (request instanceof http.ClientRequest) {
|
|
529
|
+
const networkTimings = initializeNetworkTimings(span);
|
|
530
|
+
const maxRequestBodySize = config?.maxRequestBodySize || DEFAULT_MAX_REQUEST_BODY_SIZE$1;
|
|
531
|
+
const url$1 = request.path && request.getHeader("host") ? `${request.protocol || "http:"}//${request.getHeader("host")}${request.path}` : void 0;
|
|
532
|
+
const originalWrite = request.write.bind(request);
|
|
533
|
+
const originalEnd = request.end.bind(request);
|
|
534
|
+
request.write = (data) => {
|
|
535
|
+
if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
|
|
536
|
+
return originalWrite(data);
|
|
537
|
+
};
|
|
538
|
+
request.end = (data) => {
|
|
539
|
+
if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
|
|
540
|
+
return originalEnd(data);
|
|
541
|
+
};
|
|
542
|
+
request.on("socket", (socket) => {
|
|
543
|
+
socket.on("lookup", () => {
|
|
544
|
+
networkTimings.dnsLookupAt = Date.now();
|
|
545
|
+
});
|
|
546
|
+
socket.on("connect", () => {
|
|
547
|
+
networkTimings.tcpConnectionAt = Date.now();
|
|
548
|
+
});
|
|
549
|
+
socket.on("secureConnect", () => {
|
|
550
|
+
networkTimings.tlsHandshakeAt = Date.now();
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (originalRequestHook) originalRequestHook(span, request);
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
_createResponseHook(originalResponseHook, config) {
|
|
558
|
+
return (span, response) => {
|
|
559
|
+
const headers = response.headers;
|
|
560
|
+
if (headers) captureResponseHeaders(span, headers);
|
|
561
|
+
if (response instanceof http.IncomingMessage) {
|
|
562
|
+
const networkTimings = span[NETWORK_TIMINGS_PROP_NAME];
|
|
563
|
+
const maxResponseBodySize = config?.maxResponseBodySize || DEFAULT_MAX_RESPONSE_BODY_SIZE$1;
|
|
564
|
+
const url$1 = response.url || void 0;
|
|
565
|
+
let chunks = [];
|
|
566
|
+
let totalSize = 0;
|
|
567
|
+
const shouldCapture = shouldCaptureResponseBody$1(url$1);
|
|
568
|
+
response.prependListener("data", (chunk) => {
|
|
569
|
+
if (!chunk || !shouldCapture) return;
|
|
570
|
+
if (typeof chunk === "string" || chunk instanceof Buffer) {
|
|
571
|
+
totalSize += chunk.length;
|
|
572
|
+
if (chunks && totalSize <= maxResponseBodySize) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
573
|
+
else chunks = null;
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
response.prependOnceListener("end", () => {
|
|
577
|
+
if (networkTimings) {
|
|
578
|
+
networkTimings.endAt = Date.now();
|
|
579
|
+
processNetworkTimings(span, networkTimings);
|
|
580
|
+
}
|
|
581
|
+
captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, url$1);
|
|
582
|
+
});
|
|
583
|
+
if (networkTimings) response.once("readable", () => {
|
|
584
|
+
networkTimings.firstByteAt = Date.now();
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (originalResponseHook) originalResponseHook(span, response);
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/instrumentations/http/http.ts
|
|
594
|
+
/**
|
|
595
|
+
* HTTP instrumentation for OpenTelemetry
|
|
596
|
+
*/
|
|
597
|
+
/**
|
|
598
|
+
* Creates an HTTP instrumentation instance
|
|
599
|
+
*
|
|
600
|
+
* @param isGlobalInstrumentationEnabled - Function that checks if global instrumentation is enabled
|
|
601
|
+
* @param config - Optional configuration for the instrumentation
|
|
602
|
+
* @returns PingopsHttpInstrumentation instance
|
|
603
|
+
*/
|
|
604
|
+
function createHttpInstrumentation(isGlobalInstrumentationEnabled, config) {
|
|
605
|
+
return new PingopsHttpInstrumentation({
|
|
606
|
+
ignoreIncomingRequestHook: () => true,
|
|
607
|
+
ignoreOutgoingRequestHook: () => {
|
|
608
|
+
if (isGlobalInstrumentationEnabled()) return false;
|
|
609
|
+
return _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_HTTP_ENABLED) !== true;
|
|
610
|
+
},
|
|
611
|
+
...config
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/instrumentations/undici/pingops-undici.ts
|
|
617
|
+
const DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024;
|
|
618
|
+
const DEFAULT_MAX_RESPONSE_BODY_SIZE = 4 * 1024;
|
|
619
|
+
const HTTP_REQUEST_BODY = "http.request.body";
|
|
620
|
+
const HTTP_RESPONSE_BODY = "http.response.body";
|
|
621
|
+
/**
|
|
622
|
+
* Extracts domain from URL
|
|
623
|
+
*/
|
|
624
|
+
function extractDomainFromUrl(url$1) {
|
|
625
|
+
try {
|
|
626
|
+
return new url.URL(url$1).hostname;
|
|
627
|
+
} catch {
|
|
628
|
+
const match = url$1.match(/^(?:https?:\/\/)?([^/]+)/);
|
|
629
|
+
return match ? match[1] : "";
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Gets domain rule configuration for a given URL
|
|
634
|
+
*/
|
|
635
|
+
function getDomainRule(url$1, domainAllowList) {
|
|
636
|
+
if (!domainAllowList) return;
|
|
637
|
+
const domain = extractDomainFromUrl(url$1);
|
|
638
|
+
for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Determines if request body should be captured based on priority:
|
|
642
|
+
* context > domain rule > global config > default (false)
|
|
643
|
+
*/
|
|
644
|
+
function shouldCaptureRequestBody(url$1) {
|
|
645
|
+
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_REQUEST_BODY);
|
|
646
|
+
if (contextValue !== void 0) return contextValue;
|
|
647
|
+
if (url$1) {
|
|
648
|
+
const domainRule = getDomainRule(url$1, getGlobalConfig()?.domainAllowList);
|
|
649
|
+
if (domainRule?.captureRequestBody !== void 0) return domainRule.captureRequestBody;
|
|
650
|
+
}
|
|
651
|
+
const globalConfig$1 = getGlobalConfig();
|
|
652
|
+
if (globalConfig$1?.captureRequestBody !== void 0) return globalConfig$1.captureRequestBody;
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Determines if response body should be captured based on priority:
|
|
657
|
+
* context > domain rule > global config > default (false)
|
|
658
|
+
*/
|
|
659
|
+
function shouldCaptureResponseBody(url$1) {
|
|
660
|
+
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_RESPONSE_BODY);
|
|
661
|
+
if (contextValue !== void 0) return contextValue;
|
|
662
|
+
if (url$1) {
|
|
663
|
+
const domainRule = getDomainRule(url$1, getGlobalConfig()?.domainAllowList);
|
|
664
|
+
if (domainRule?.captureResponseBody !== void 0) return domainRule.captureResponseBody;
|
|
665
|
+
}
|
|
666
|
+
const globalConfig$1 = getGlobalConfig();
|
|
667
|
+
if (globalConfig$1?.captureResponseBody !== void 0) return globalConfig$1.captureResponseBody;
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
var UndiciInstrumentation = class extends _opentelemetry_instrumentation.InstrumentationBase {
|
|
671
|
+
_recordFromReq = /* @__PURE__ */ new WeakMap();
|
|
672
|
+
constructor(config = {}) {
|
|
673
|
+
super("pingops-undici", "0.1.0", config);
|
|
674
|
+
}
|
|
675
|
+
init() {}
|
|
676
|
+
disable() {
|
|
677
|
+
super.disable();
|
|
678
|
+
this._channelSubs.forEach((sub) => sub.unsubscribe());
|
|
679
|
+
this._channelSubs.length = 0;
|
|
680
|
+
}
|
|
681
|
+
enable() {
|
|
682
|
+
super.enable();
|
|
683
|
+
this._channelSubs = this._channelSubs || [];
|
|
684
|
+
if (this._channelSubs.length > 0) return;
|
|
685
|
+
this.subscribeToChannel("undici:request:create", this.onRequestCreated.bind(this));
|
|
686
|
+
this.subscribeToChannel("undici:client:sendHeaders", this.onRequestHeaders.bind(this));
|
|
687
|
+
this.subscribeToChannel("undici:request:headers", this.onResponseHeaders.bind(this));
|
|
688
|
+
this.subscribeToChannel("undici:request:trailers", this.onDone.bind(this));
|
|
689
|
+
this.subscribeToChannel("undici:request:error", this.onError.bind(this));
|
|
690
|
+
this.subscribeToChannel("undici:request:bodyChunkSent", this.onBodyChunkSent.bind(this));
|
|
691
|
+
this.subscribeToChannel("undici:request:bodySent", this.onBodySent.bind(this));
|
|
692
|
+
this.subscribeToChannel("undici:request:bodyChunkReceived", this.onBodyChunkReceived.bind(this));
|
|
693
|
+
}
|
|
694
|
+
_updateMetricInstruments() {
|
|
695
|
+
this._httpClientDurationHistogram = this.meter.createHistogram(_opentelemetry_semantic_conventions.METRIC_HTTP_CLIENT_REQUEST_DURATION, {
|
|
696
|
+
description: "Measures the duration of outbound HTTP requests.",
|
|
697
|
+
unit: "s",
|
|
698
|
+
valueType: _opentelemetry_api.ValueType.DOUBLE,
|
|
699
|
+
advice: { explicitBucketBoundaries: [
|
|
700
|
+
.005,
|
|
701
|
+
.01,
|
|
702
|
+
.025,
|
|
703
|
+
.05,
|
|
704
|
+
.075,
|
|
705
|
+
.1,
|
|
706
|
+
.25,
|
|
707
|
+
.5,
|
|
708
|
+
.75,
|
|
709
|
+
1,
|
|
710
|
+
2.5,
|
|
711
|
+
5,
|
|
712
|
+
7.5,
|
|
713
|
+
10
|
|
714
|
+
] }
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
subscribeToChannel(diagnosticChannel, onMessage) {
|
|
718
|
+
const [major, minor] = process.version.replace("v", "").split(".").map((n) => Number(n));
|
|
719
|
+
const useNewSubscribe = major > 18 || major === 18 && minor >= 19;
|
|
720
|
+
let unsubscribe;
|
|
721
|
+
if (useNewSubscribe) {
|
|
722
|
+
diagnostics_channel.subscribe?.(diagnosticChannel, onMessage);
|
|
723
|
+
unsubscribe = () => diagnostics_channel.unsubscribe?.(diagnosticChannel, onMessage);
|
|
724
|
+
} else {
|
|
725
|
+
const channel = diagnostics_channel.channel(diagnosticChannel);
|
|
726
|
+
channel.subscribe(onMessage);
|
|
727
|
+
unsubscribe = () => channel.unsubscribe(onMessage);
|
|
728
|
+
}
|
|
729
|
+
this._channelSubs.push({
|
|
730
|
+
name: diagnosticChannel,
|
|
731
|
+
unsubscribe
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
parseRequestHeaders(request) {
|
|
735
|
+
const result = /* @__PURE__ */ new Map();
|
|
736
|
+
if (Array.isArray(request.headers)) for (let i = 0; i < request.headers.length; i += 2) {
|
|
737
|
+
const key = request.headers[i];
|
|
738
|
+
const value = request.headers[i + 1];
|
|
739
|
+
if (typeof key === "string") result.set(key.toLowerCase(), value);
|
|
740
|
+
}
|
|
741
|
+
else if (typeof request.headers === "string") {
|
|
742
|
+
const headers = request.headers.split("\r\n");
|
|
743
|
+
for (const line of headers) {
|
|
744
|
+
if (!line) continue;
|
|
745
|
+
const colonIndex = line.indexOf(":");
|
|
746
|
+
if (colonIndex === -1) continue;
|
|
747
|
+
const key = line.substring(0, colonIndex).toLowerCase();
|
|
748
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
749
|
+
const allValues = result.get(key);
|
|
750
|
+
if (allValues && Array.isArray(allValues)) allValues.push(value);
|
|
751
|
+
else if (allValues) result.set(key, [allValues, value]);
|
|
752
|
+
else result.set(key, value);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
onRequestCreated({ request }) {
|
|
758
|
+
const config = this.getConfig();
|
|
759
|
+
const enabled = config.enabled !== false;
|
|
760
|
+
if ((0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => !enabled || request.method === "CONNECT" || config.ignoreRequestHook?.(request), (e) => e && this._diag.error("caught ignoreRequestHook error: ", e), true)) return;
|
|
761
|
+
const startTime = (0, _opentelemetry_core.hrTime)();
|
|
762
|
+
let requestUrl;
|
|
763
|
+
try {
|
|
764
|
+
requestUrl = new url.URL(request.path, request.origin);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
this._diag.warn("could not determine url.full:", err);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const urlScheme = requestUrl.protocol.replace(":", "");
|
|
770
|
+
const requestMethod = this.getRequestMethod(request.method);
|
|
771
|
+
const attributes = {
|
|
772
|
+
[_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD]: requestMethod,
|
|
773
|
+
[_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD_ORIGINAL]: request.method,
|
|
774
|
+
[_opentelemetry_semantic_conventions.ATTR_URL_FULL]: requestUrl.toString(),
|
|
775
|
+
[_opentelemetry_semantic_conventions.ATTR_URL_PATH]: requestUrl.pathname,
|
|
776
|
+
[_opentelemetry_semantic_conventions.ATTR_URL_QUERY]: requestUrl.search,
|
|
777
|
+
[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME]: urlScheme
|
|
778
|
+
};
|
|
779
|
+
const schemePorts = {
|
|
780
|
+
https: "443",
|
|
781
|
+
http: "80"
|
|
782
|
+
};
|
|
783
|
+
const serverAddress = requestUrl.hostname;
|
|
784
|
+
const serverPort = requestUrl.port || schemePorts[urlScheme];
|
|
785
|
+
attributes[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] = serverAddress;
|
|
786
|
+
if (serverPort && !isNaN(Number(serverPort))) attributes[_opentelemetry_semantic_conventions.ATTR_SERVER_PORT] = Number(serverPort);
|
|
787
|
+
const userAgentValues = this.parseRequestHeaders(request).get("user-agent");
|
|
788
|
+
if (userAgentValues) attributes[_opentelemetry_semantic_conventions.ATTR_USER_AGENT_ORIGINAL] = Array.isArray(userAgentValues) ? userAgentValues[userAgentValues.length - 1] : userAgentValues;
|
|
789
|
+
const hookAttributes = (0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => config.startSpanHook?.(request), (e) => e && this._diag.error("caught startSpanHook error: ", e), true);
|
|
790
|
+
if (hookAttributes) Object.entries(hookAttributes).forEach(([key, val]) => {
|
|
791
|
+
attributes[key] = val;
|
|
792
|
+
});
|
|
793
|
+
const activeCtx = _opentelemetry_api.context.active();
|
|
794
|
+
const currentSpan = _opentelemetry_api.trace.getSpan(activeCtx);
|
|
795
|
+
let span;
|
|
796
|
+
if (config.requireParentforSpans && (!currentSpan || !_opentelemetry_api.trace.isSpanContextValid(currentSpan.spanContext()))) span = _opentelemetry_api.trace.wrapSpanContext(_opentelemetry_api.INVALID_SPAN_CONTEXT);
|
|
797
|
+
else span = this.tracer.startSpan(requestMethod === "_OTHER" ? "HTTP" : requestMethod, {
|
|
798
|
+
kind: _opentelemetry_api.SpanKind.CLIENT,
|
|
799
|
+
attributes
|
|
800
|
+
}, activeCtx);
|
|
801
|
+
(0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => config.requestHook?.(span, request), (e) => e && this._diag.error("caught requestHook error: ", e), true);
|
|
802
|
+
const requestContext = _opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), span);
|
|
803
|
+
const addedHeaders = {};
|
|
804
|
+
_opentelemetry_api.propagation.inject(requestContext, addedHeaders);
|
|
805
|
+
const headerEntries = Object.entries(addedHeaders);
|
|
806
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
807
|
+
const [k, v] = headerEntries[i];
|
|
808
|
+
if (typeof request.addHeader === "function") request.addHeader(k, v);
|
|
809
|
+
else if (typeof request.headers === "string") request.headers += `${k}: ${v}\r\n`;
|
|
810
|
+
else if (Array.isArray(request.headers)) request.headers.push(k, v);
|
|
811
|
+
}
|
|
812
|
+
this._recordFromReq.set(request, {
|
|
813
|
+
span,
|
|
814
|
+
attributes,
|
|
815
|
+
startTime,
|
|
816
|
+
requestBodyChunks: [],
|
|
817
|
+
responseBodyChunks: [],
|
|
818
|
+
requestBodySize: 0,
|
|
819
|
+
responseBodySize: 0,
|
|
820
|
+
url: requestUrl.toString()
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
onRequestHeaders({ request, socket }) {
|
|
824
|
+
const record = this._recordFromReq.get(request);
|
|
825
|
+
if (!record) return;
|
|
826
|
+
const { span } = record;
|
|
827
|
+
const { remoteAddress, remotePort } = socket;
|
|
828
|
+
const spanAttributes = {
|
|
829
|
+
[_opentelemetry_semantic_conventions.ATTR_NETWORK_PEER_ADDRESS]: remoteAddress,
|
|
830
|
+
[_opentelemetry_semantic_conventions.ATTR_NETWORK_PEER_PORT]: remotePort
|
|
831
|
+
};
|
|
832
|
+
const headersMap = this.parseRequestHeaders(request);
|
|
833
|
+
for (const [name, value] of headersMap.entries()) {
|
|
834
|
+
const attrValue = Array.isArray(value) ? value.join(", ") : value;
|
|
835
|
+
spanAttributes[`http.request.header.${name}`] = attrValue;
|
|
836
|
+
}
|
|
837
|
+
span.setAttributes(spanAttributes);
|
|
838
|
+
}
|
|
839
|
+
onResponseHeaders({ request, response }) {
|
|
840
|
+
const record = this._recordFromReq.get(request);
|
|
841
|
+
if (!record) return;
|
|
842
|
+
const { span, attributes } = record;
|
|
843
|
+
const spanAttributes = { [_opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE]: response.statusCode };
|
|
844
|
+
const config = this.getConfig();
|
|
845
|
+
(0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => config.responseHook?.(span, {
|
|
846
|
+
request,
|
|
847
|
+
response
|
|
848
|
+
}), (e) => e && this._diag.error("caught responseHook error: ", e), true);
|
|
849
|
+
for (let idx = 0; idx < response.headers.length; idx = idx + 2) {
|
|
850
|
+
const name = response.headers[idx].toString().toLowerCase();
|
|
851
|
+
const value = response.headers[idx + 1];
|
|
852
|
+
spanAttributes[`http.response.header.${name}`] = value.toString();
|
|
853
|
+
if (name === "content-length") {
|
|
854
|
+
const contentLength = Number(value.toString());
|
|
855
|
+
if (!isNaN(contentLength)) spanAttributes["http.response.header.content-length"] = contentLength;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
span.setAttributes(spanAttributes);
|
|
859
|
+
span.setStatus({ code: response.statusCode >= 400 ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.UNSET });
|
|
860
|
+
record.attributes = Object.assign(attributes, spanAttributes);
|
|
861
|
+
}
|
|
862
|
+
onDone({ request }) {
|
|
863
|
+
const record = this._recordFromReq.get(request);
|
|
864
|
+
if (!record) return;
|
|
865
|
+
const { span, attributes, startTime } = record;
|
|
866
|
+
if (shouldCaptureResponseBody(record.url)) {
|
|
867
|
+
if (record.responseBodyChunks.length > 0 && record.responseBodySize !== Infinity) try {
|
|
868
|
+
const responseBody = Buffer.concat(record.responseBodyChunks).toString("utf-8");
|
|
869
|
+
if (responseBody) span.setAttribute(HTTP_RESPONSE_BODY, responseBody);
|
|
870
|
+
} catch (e) {
|
|
871
|
+
this._diag.error("Error occurred while capturing response body:", e);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
span.end();
|
|
875
|
+
this._recordFromReq.delete(request);
|
|
876
|
+
this.recordRequestDuration(attributes, startTime);
|
|
877
|
+
}
|
|
878
|
+
onError({ request, error }) {
|
|
879
|
+
const record = this._recordFromReq.get(request);
|
|
880
|
+
if (!record) return;
|
|
881
|
+
const { span, attributes, startTime } = record;
|
|
882
|
+
if (shouldCaptureRequestBody(record.url)) {
|
|
883
|
+
if (record.requestBodyChunks.length > 0 && record.requestBodySize !== Infinity) try {
|
|
884
|
+
const requestBody = Buffer.concat(record.requestBodyChunks).toString("utf-8");
|
|
885
|
+
if (requestBody) span.setAttribute(HTTP_REQUEST_BODY, requestBody);
|
|
886
|
+
} catch (e) {
|
|
887
|
+
this._diag.error("Error occurred while capturing request body:", e);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
span.recordException(error);
|
|
891
|
+
span.setStatus({
|
|
892
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
893
|
+
message: error.message
|
|
894
|
+
});
|
|
895
|
+
span.end();
|
|
896
|
+
this._recordFromReq.delete(request);
|
|
897
|
+
attributes[_opentelemetry_semantic_conventions.ATTR_ERROR_TYPE] = error.message;
|
|
898
|
+
this.recordRequestDuration(attributes, startTime);
|
|
899
|
+
}
|
|
900
|
+
onBodyChunkSent({ request, chunk }) {
|
|
901
|
+
const record = this._recordFromReq.get(request);
|
|
902
|
+
if (!record) return;
|
|
903
|
+
if (!shouldCaptureRequestBody(record.url)) return;
|
|
904
|
+
const maxRequestBodySize = this.getConfig().maxRequestBodySize ?? DEFAULT_MAX_REQUEST_BODY_SIZE;
|
|
905
|
+
if (record.requestBodySize + chunk.length <= maxRequestBodySize) {
|
|
906
|
+
record.requestBodyChunks.push(chunk);
|
|
907
|
+
record.requestBodySize += chunk.length;
|
|
908
|
+
} else if (record.requestBodyChunks.length === 0) record.requestBodySize = Infinity;
|
|
909
|
+
}
|
|
910
|
+
onBodySent({ request }) {
|
|
911
|
+
const record = this._recordFromReq.get(request);
|
|
912
|
+
if (!record) return;
|
|
913
|
+
if (!shouldCaptureRequestBody(record.url)) {
|
|
914
|
+
record.requestBodyChunks = [];
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (record.requestBodyChunks.length > 0 && record.requestBodySize !== Infinity) try {
|
|
918
|
+
const requestBody = Buffer.concat(record.requestBodyChunks).toString("utf-8");
|
|
919
|
+
if (requestBody) record.span.setAttribute(HTTP_REQUEST_BODY, requestBody);
|
|
920
|
+
} catch (e) {
|
|
921
|
+
this._diag.error("Error occurred while capturing request body:", e);
|
|
922
|
+
}
|
|
923
|
+
record.requestBodyChunks = [];
|
|
924
|
+
}
|
|
925
|
+
onBodyChunkReceived({ request, chunk }) {
|
|
926
|
+
const record = this._recordFromReq.get(request);
|
|
927
|
+
if (!record) return;
|
|
928
|
+
if (!shouldCaptureResponseBody(record.url)) return;
|
|
929
|
+
const maxResponseBodySize = this.getConfig().maxResponseBodySize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE;
|
|
930
|
+
if (record.responseBodySize + chunk.length <= maxResponseBodySize) {
|
|
931
|
+
record.responseBodyChunks.push(chunk);
|
|
932
|
+
record.responseBodySize += chunk.length;
|
|
933
|
+
} else if (record.responseBodyChunks.length === 0) record.responseBodySize = Infinity;
|
|
934
|
+
}
|
|
935
|
+
recordRequestDuration(attributes, startTime) {
|
|
936
|
+
const metricsAttributes = {};
|
|
937
|
+
[
|
|
938
|
+
_opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE,
|
|
939
|
+
_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD,
|
|
940
|
+
_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS,
|
|
941
|
+
_opentelemetry_semantic_conventions.ATTR_SERVER_PORT,
|
|
942
|
+
_opentelemetry_semantic_conventions.ATTR_URL_SCHEME,
|
|
943
|
+
_opentelemetry_semantic_conventions.ATTR_ERROR_TYPE
|
|
944
|
+
].forEach((key) => {
|
|
945
|
+
if (key in attributes) metricsAttributes[key] = attributes[key];
|
|
946
|
+
});
|
|
947
|
+
const durationSeconds = (0, _opentelemetry_core.hrTimeToMilliseconds)((0, _opentelemetry_core.hrTimeDuration)(startTime, (0, _opentelemetry_core.hrTime)())) / 1e3;
|
|
948
|
+
this._httpClientDurationHistogram.record(durationSeconds, metricsAttributes);
|
|
949
|
+
}
|
|
950
|
+
getRequestMethod(original) {
|
|
951
|
+
if (original.toUpperCase() in {
|
|
952
|
+
CONNECT: true,
|
|
953
|
+
OPTIONS: true,
|
|
954
|
+
HEAD: true,
|
|
955
|
+
GET: true,
|
|
956
|
+
POST: true,
|
|
957
|
+
PUT: true,
|
|
958
|
+
PATCH: true,
|
|
959
|
+
DELETE: true,
|
|
960
|
+
TRACE: true
|
|
961
|
+
}) return original.toUpperCase();
|
|
962
|
+
return "_OTHER";
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/instrumentations/undici/undici.ts
|
|
968
|
+
/**
|
|
969
|
+
* Undici instrumentation for OpenTelemetry
|
|
970
|
+
*/
|
|
971
|
+
/**
|
|
972
|
+
* Creates an Undici instrumentation instance
|
|
973
|
+
*
|
|
974
|
+
* @param isGlobalInstrumentationEnabled - Function that checks if global instrumentation is enabled
|
|
975
|
+
* @returns UndiciInstrumentation instance
|
|
976
|
+
*/
|
|
977
|
+
function createUndiciInstrumentation(isGlobalInstrumentationEnabled) {
|
|
978
|
+
return new UndiciInstrumentation({
|
|
979
|
+
enabled: true,
|
|
980
|
+
ignoreRequestHook: () => {
|
|
981
|
+
if (isGlobalInstrumentationEnabled()) return false;
|
|
982
|
+
return _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_HTTP_ENABLED) !== true;
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/instrumentations/index.ts
|
|
989
|
+
let installed = false;
|
|
990
|
+
/**
|
|
991
|
+
* Registers instrumentations for Node.js environment.
|
|
992
|
+
* This function is idempotent and can be called multiple times safely.
|
|
993
|
+
*
|
|
994
|
+
* Instrumentation behavior:
|
|
995
|
+
* - If global instrumentation is enabled: all HTTP requests are instrumented
|
|
996
|
+
* - If global instrumentation is NOT enabled: only requests within wrapHttp blocks are instrumented
|
|
997
|
+
*
|
|
998
|
+
* @param isGlobalInstrumentationEnabled - Function that checks if global instrumentation is enabled
|
|
999
|
+
* @returns Array of Instrumentation instances
|
|
1000
|
+
*/
|
|
1001
|
+
function getInstrumentations(isGlobalInstrumentationEnabled) {
|
|
1002
|
+
if (installed) return [];
|
|
1003
|
+
installed = true;
|
|
1004
|
+
return [createHttpInstrumentation(isGlobalInstrumentationEnabled), createUndiciInstrumentation(isGlobalInstrumentationEnabled)];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
//#endregion
|
|
1008
|
+
exports.PingopsHttpInstrumentation = PingopsHttpInstrumentation;
|
|
1009
|
+
exports.PingopsHttpSemanticAttributes = PingopsHttpSemanticAttributes;
|
|
1010
|
+
exports.PingopsSemanticAttributes = PingopsSemanticAttributes;
|
|
1011
|
+
exports.PingopsSpanProcessor = PingopsSpanProcessor;
|
|
1012
|
+
exports.createHttpInstrumentation = createHttpInstrumentation;
|
|
1013
|
+
exports.createUndiciInstrumentation = createUndiciInstrumentation;
|
|
1014
|
+
exports.getInstrumentations = getInstrumentations;
|
|
1015
|
+
exports.getPingopsTracerProvider = getPingopsTracerProvider;
|
|
1016
|
+
exports.setPingopsTracerProvider = setPingopsTracerProvider;
|
|
1017
|
+
exports.shutdownTracerProvider = shutdownTracerProvider;
|
|
1018
|
+
//# sourceMappingURL=index.cjs.map
|