@pingops/otel 0.1.1 → 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.
Files changed (53) hide show
  1. package/dist/index.cjs +1018 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.cts +342 -0
  4. package/dist/index.d.cts.map +1 -0
  5. package/dist/index.d.mts +342 -0
  6. package/dist/index.d.mts.map +1 -0
  7. package/dist/index.mjs +981 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +18 -6
  10. package/dist/config-store.d.ts +0 -26
  11. package/dist/config-store.d.ts.map +0 -1
  12. package/dist/config-store.js +0 -26
  13. package/dist/config-store.js.map +0 -1
  14. package/dist/config.d.ts +0 -83
  15. package/dist/config.d.ts.map +0 -1
  16. package/dist/config.js +0 -5
  17. package/dist/config.js.map +0 -1
  18. package/dist/index.d.ts +0 -11
  19. package/dist/index.d.ts.map +0 -1
  20. package/dist/index.js +0 -10
  21. package/dist/index.js.map +0 -1
  22. package/dist/instrumentations/http/http.d.ts +0 -13
  23. package/dist/instrumentations/http/http.d.ts.map +0 -1
  24. package/dist/instrumentations/http/http.js +0 -28
  25. package/dist/instrumentations/http/http.js.map +0 -1
  26. package/dist/instrumentations/http/pingops-http.d.ts +0 -52
  27. package/dist/instrumentations/http/pingops-http.d.ts.map +0 -1
  28. package/dist/instrumentations/http/pingops-http.js +0 -381
  29. package/dist/instrumentations/http/pingops-http.js.map +0 -1
  30. package/dist/instrumentations/index.d.ts +0 -17
  31. package/dist/instrumentations/index.d.ts.map +0 -1
  32. package/dist/instrumentations/index.js +0 -28
  33. package/dist/instrumentations/index.js.map +0 -1
  34. package/dist/instrumentations/undici/pingops-undici.d.ts +0 -25
  35. package/dist/instrumentations/undici/pingops-undici.d.ts.map +0 -1
  36. package/dist/instrumentations/undici/pingops-undici.js +0 -568
  37. package/dist/instrumentations/undici/pingops-undici.js.map +0 -1
  38. package/dist/instrumentations/undici/types.d.ts +0 -106
  39. package/dist/instrumentations/undici/types.d.ts.map +0 -1
  40. package/dist/instrumentations/undici/types.js +0 -2
  41. package/dist/instrumentations/undici/types.js.map +0 -1
  42. package/dist/instrumentations/undici/undici.d.ts +0 -12
  43. package/dist/instrumentations/undici/undici.d.ts.map +0 -1
  44. package/dist/instrumentations/undici/undici.js +0 -26
  45. package/dist/instrumentations/undici/undici.js.map +0 -1
  46. package/dist/span-processor.d.ts +0 -78
  47. package/dist/span-processor.d.ts.map +0 -1
  48. package/dist/span-processor.js +0 -282
  49. package/dist/span-processor.js.map +0 -1
  50. package/dist/tracer-provider.d.ts +0 -57
  51. package/dist/tracer-provider.d.ts.map +0 -1
  52. package/dist/tracer-provider.js +0 -184
  53. 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