@pingops/core 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +844 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +305 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +305 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +824 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +17 -5
- package/dist/context-keys.d.ts +0 -42
- package/dist/context-keys.d.ts.map +0 -1
- package/dist/context-keys.js +0 -43
- package/dist/context-keys.js.map +0 -1
- package/dist/filtering/domain-filter.d.ts +0 -9
- package/dist/filtering/domain-filter.d.ts.map +0 -1
- package/dist/filtering/domain-filter.js +0 -136
- package/dist/filtering/domain-filter.js.map +0 -1
- package/dist/filtering/header-filter.d.ts +0 -31
- package/dist/filtering/header-filter.d.ts.map +0 -1
- package/dist/filtering/header-filter.js +0 -187
- package/dist/filtering/header-filter.js.map +0 -1
- package/dist/filtering/span-filter.d.ts +0 -13
- package/dist/filtering/span-filter.d.ts.map +0 -1
- package/dist/filtering/span-filter.js +0 -46
- package/dist/filtering/span-filter.js.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -13
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -21
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -36
- package/dist/logger.js.map +0 -1
- package/dist/types.d.ts +0 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/utils/context-extractor.d.ts +0 -13
- package/dist/utils/context-extractor.d.ts.map +0 -1
- package/dist/utils/context-extractor.js +0 -44
- package/dist/utils/context-extractor.js.map +0 -1
- package/dist/utils/span-extractor.d.ts +0 -10
- package/dist/utils/span-extractor.d.ts.map +0 -1
- package/dist/utils/span-extractor.js +0 -156
- package/dist/utils/span-extractor.js.map +0 -1
- package/dist/wrap-http.d.ts +0 -55
- package/dist/wrap-http.d.ts.map +0 -1
- package/dist/wrap-http.js +0 -135
- package/dist/wrap-http.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
let _opentelemetry_api = require("@opentelemetry/api");
|
|
2
|
+
|
|
3
|
+
//#region src/logger.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates a logger instance with a specific prefix
|
|
6
|
+
*
|
|
7
|
+
* @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')
|
|
8
|
+
* @returns Logger instance
|
|
9
|
+
*/
|
|
10
|
+
function createLogger(prefix) {
|
|
11
|
+
const isDebugEnabled = process.env.PINGOPS_DEBUG === "true";
|
|
12
|
+
const formatMessage = (level, message) => {
|
|
13
|
+
return `[${(/* @__PURE__ */ new Date()).toISOString()}] ${prefix} [${level.toUpperCase()}] ${message}`;
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
debug(message, ...args) {
|
|
17
|
+
if (isDebugEnabled) console.debug(formatMessage("debug", message), ...args);
|
|
18
|
+
},
|
|
19
|
+
info(message, ...args) {
|
|
20
|
+
console.log(formatMessage("info", message), ...args);
|
|
21
|
+
},
|
|
22
|
+
warn(message, ...args) {
|
|
23
|
+
console.warn(formatMessage("warn", message), ...args);
|
|
24
|
+
},
|
|
25
|
+
error(message, ...args) {
|
|
26
|
+
console.error(formatMessage("error", message), ...args);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/filtering/span-filter.ts
|
|
33
|
+
/**
|
|
34
|
+
* Span filtering logic - determines if a span is eligible for capture
|
|
35
|
+
*/
|
|
36
|
+
const log$2 = createLogger("[PingOps SpanFilter]");
|
|
37
|
+
/**
|
|
38
|
+
* Checks if a span is eligible for capture based on span kind and attributes.
|
|
39
|
+
* A span is eligible if:
|
|
40
|
+
* 1. span.kind === SpanKind.CLIENT
|
|
41
|
+
* 2. AND has HTTP attributes (http.method, http.url, or server.address)
|
|
42
|
+
* OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)
|
|
43
|
+
*/
|
|
44
|
+
function isSpanEligible(span) {
|
|
45
|
+
log$2.debug("Checking span eligibility", {
|
|
46
|
+
spanName: span.name,
|
|
47
|
+
spanKind: span.kind,
|
|
48
|
+
spanId: span.spanContext().spanId,
|
|
49
|
+
traceId: span.spanContext().traceId
|
|
50
|
+
});
|
|
51
|
+
if (span.kind !== _opentelemetry_api.SpanKind.CLIENT) {
|
|
52
|
+
log$2.debug("Span not eligible: not CLIENT kind", {
|
|
53
|
+
spanName: span.name,
|
|
54
|
+
spanKind: span.kind
|
|
55
|
+
});
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const attributes = span.attributes;
|
|
59
|
+
const hasHttpMethod = attributes["http.method"] !== void 0;
|
|
60
|
+
const hasHttpUrl = attributes["http.url"] !== void 0;
|
|
61
|
+
const hasServerAddress = attributes["server.address"] !== void 0;
|
|
62
|
+
const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;
|
|
63
|
+
log$2.debug("Span eligibility check result", {
|
|
64
|
+
spanName: span.name,
|
|
65
|
+
isEligible,
|
|
66
|
+
httpAttributes: {
|
|
67
|
+
hasMethod: hasHttpMethod,
|
|
68
|
+
hasUrl: hasHttpUrl,
|
|
69
|
+
hasServerAddress
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return isEligible;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/filtering/domain-filter.ts
|
|
77
|
+
const log$1 = createLogger("[PingOps DomainFilter]");
|
|
78
|
+
/**
|
|
79
|
+
* Extracts domain from a URL
|
|
80
|
+
*/
|
|
81
|
+
function extractDomain(url) {
|
|
82
|
+
try {
|
|
83
|
+
const domain = new URL(url).hostname;
|
|
84
|
+
log$1.debug("Extracted domain from URL", {
|
|
85
|
+
url,
|
|
86
|
+
domain
|
|
87
|
+
});
|
|
88
|
+
return domain;
|
|
89
|
+
} catch {
|
|
90
|
+
const match = url.match(/^(?:https?:\/\/)?([^/]+)/);
|
|
91
|
+
const domain = match ? match[1] : "";
|
|
92
|
+
log$1.debug("Extracted domain from URL (fallback)", {
|
|
93
|
+
url,
|
|
94
|
+
domain
|
|
95
|
+
});
|
|
96
|
+
return domain;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Checks if a domain matches a rule (exact or suffix match)
|
|
101
|
+
*/
|
|
102
|
+
function domainMatches(domain, ruleDomain) {
|
|
103
|
+
if (domain === ruleDomain) {
|
|
104
|
+
log$1.debug("Domain exact match", {
|
|
105
|
+
domain,
|
|
106
|
+
ruleDomain
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (ruleDomain.startsWith(".")) {
|
|
111
|
+
const matches = domain.endsWith(ruleDomain) || domain === ruleDomain.slice(1);
|
|
112
|
+
log$1.debug("Domain suffix match check", {
|
|
113
|
+
domain,
|
|
114
|
+
ruleDomain,
|
|
115
|
+
matches
|
|
116
|
+
});
|
|
117
|
+
return matches;
|
|
118
|
+
}
|
|
119
|
+
log$1.debug("Domain does not match", {
|
|
120
|
+
domain,
|
|
121
|
+
ruleDomain
|
|
122
|
+
});
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Checks if a path matches any of the allowed paths (prefix match)
|
|
127
|
+
*/
|
|
128
|
+
function pathMatches(path, allowedPaths) {
|
|
129
|
+
if (!allowedPaths || allowedPaths.length === 0) {
|
|
130
|
+
log$1.debug("No path restrictions, all paths match", { path });
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
const matches = allowedPaths.some((allowedPath) => path.startsWith(allowedPath));
|
|
134
|
+
log$1.debug("Path match check", {
|
|
135
|
+
path,
|
|
136
|
+
allowedPaths,
|
|
137
|
+
matches
|
|
138
|
+
});
|
|
139
|
+
return matches;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Determines if a span should be captured based on domain rules
|
|
143
|
+
*/
|
|
144
|
+
function shouldCaptureSpan(url, domainAllowList, domainDenyList) {
|
|
145
|
+
log$1.debug("Checking domain filter rules", {
|
|
146
|
+
url,
|
|
147
|
+
hasAllowList: !!domainAllowList && domainAllowList.length > 0,
|
|
148
|
+
hasDenyList: !!domainDenyList && domainDenyList.length > 0,
|
|
149
|
+
allowListCount: domainAllowList?.length || 0,
|
|
150
|
+
denyListCount: domainDenyList?.length || 0
|
|
151
|
+
});
|
|
152
|
+
const domain = extractDomain(url);
|
|
153
|
+
let path = "/";
|
|
154
|
+
try {
|
|
155
|
+
path = new URL(url).pathname;
|
|
156
|
+
} catch {
|
|
157
|
+
const pathMatch = url.match(/^(?:https?:\/\/)?[^/]+(\/.*)?$/);
|
|
158
|
+
path = pathMatch && pathMatch[1] ? pathMatch[1] : "/";
|
|
159
|
+
}
|
|
160
|
+
log$1.debug("Extracted domain and path", {
|
|
161
|
+
url,
|
|
162
|
+
domain,
|
|
163
|
+
path
|
|
164
|
+
});
|
|
165
|
+
if (domainDenyList) {
|
|
166
|
+
for (const rule of domainDenyList) if (domainMatches(domain, rule.domain)) {
|
|
167
|
+
log$1.info("Domain denied by deny list", {
|
|
168
|
+
domain,
|
|
169
|
+
ruleDomain: rule.domain,
|
|
170
|
+
url
|
|
171
|
+
});
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
log$1.debug("Domain passed deny list check", { domain });
|
|
175
|
+
}
|
|
176
|
+
if (!domainAllowList || domainAllowList.length === 0) {
|
|
177
|
+
log$1.debug("No allow list configured, capturing span", {
|
|
178
|
+
domain,
|
|
179
|
+
url
|
|
180
|
+
});
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
for (const rule of domainAllowList) if (domainMatches(domain, rule.domain)) if (rule.paths && rule.paths.length > 0) if (pathMatches(path, rule.paths)) {
|
|
184
|
+
log$1.info("Domain and path allowed by allow list", {
|
|
185
|
+
domain,
|
|
186
|
+
ruleDomain: rule.domain,
|
|
187
|
+
path,
|
|
188
|
+
allowedPaths: rule.paths,
|
|
189
|
+
url
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
} else log$1.debug("Domain allowed but path not matched", {
|
|
193
|
+
domain,
|
|
194
|
+
ruleDomain: rule.domain,
|
|
195
|
+
path,
|
|
196
|
+
allowedPaths: rule.paths
|
|
197
|
+
});
|
|
198
|
+
else {
|
|
199
|
+
log$1.info("Domain allowed by allow list", {
|
|
200
|
+
domain,
|
|
201
|
+
ruleDomain: rule.domain,
|
|
202
|
+
url
|
|
203
|
+
});
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
log$1.info("Domain not in allow list, filtering out", {
|
|
207
|
+
domain,
|
|
208
|
+
url
|
|
209
|
+
});
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/filtering/sensitive-headers.ts
|
|
215
|
+
/**
|
|
216
|
+
* Sensitive header patterns and redaction configuration
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* Default patterns for sensitive headers that should be redacted
|
|
220
|
+
* These are matched case-insensitively
|
|
221
|
+
*/
|
|
222
|
+
const DEFAULT_SENSITIVE_HEADER_PATTERNS = [
|
|
223
|
+
"authorization",
|
|
224
|
+
"www-authenticate",
|
|
225
|
+
"proxy-authenticate",
|
|
226
|
+
"proxy-authorization",
|
|
227
|
+
"x-auth-token",
|
|
228
|
+
"x-api-key",
|
|
229
|
+
"x-api-token",
|
|
230
|
+
"x-access-token",
|
|
231
|
+
"x-auth-user",
|
|
232
|
+
"x-auth-password",
|
|
233
|
+
"x-csrf-token",
|
|
234
|
+
"x-xsrf-token",
|
|
235
|
+
"api-key",
|
|
236
|
+
"apikey",
|
|
237
|
+
"api_key",
|
|
238
|
+
"access-key",
|
|
239
|
+
"accesskey",
|
|
240
|
+
"access_key",
|
|
241
|
+
"secret-key",
|
|
242
|
+
"secretkey",
|
|
243
|
+
"secret_key",
|
|
244
|
+
"private-key",
|
|
245
|
+
"privatekey",
|
|
246
|
+
"private_key",
|
|
247
|
+
"cookie",
|
|
248
|
+
"set-cookie",
|
|
249
|
+
"session-id",
|
|
250
|
+
"sessionid",
|
|
251
|
+
"session_id",
|
|
252
|
+
"session-token",
|
|
253
|
+
"sessiontoken",
|
|
254
|
+
"session_token",
|
|
255
|
+
"oauth-token",
|
|
256
|
+
"oauth_token",
|
|
257
|
+
"oauth2-token",
|
|
258
|
+
"oauth2_token",
|
|
259
|
+
"bearer",
|
|
260
|
+
"x-amz-security-token",
|
|
261
|
+
"x-amz-signature",
|
|
262
|
+
"x-aws-access-key",
|
|
263
|
+
"x-aws-secret-key",
|
|
264
|
+
"x-aws-session-token",
|
|
265
|
+
"x-password",
|
|
266
|
+
"x-secret",
|
|
267
|
+
"x-token",
|
|
268
|
+
"x-jwt",
|
|
269
|
+
"x-jwt-token",
|
|
270
|
+
"x-refresh-token",
|
|
271
|
+
"x-client-secret",
|
|
272
|
+
"x-client-id",
|
|
273
|
+
"x-user-token",
|
|
274
|
+
"x-service-key"
|
|
275
|
+
];
|
|
276
|
+
/**
|
|
277
|
+
* Redaction strategies for sensitive header values
|
|
278
|
+
*/
|
|
279
|
+
let HeaderRedactionStrategy = /* @__PURE__ */ function(HeaderRedactionStrategy$1) {
|
|
280
|
+
/**
|
|
281
|
+
* Replace the entire value with a fixed redaction string
|
|
282
|
+
*/
|
|
283
|
+
HeaderRedactionStrategy$1["REPLACE"] = "replace";
|
|
284
|
+
/**
|
|
285
|
+
* Show only the first N characters, redact the rest
|
|
286
|
+
*/
|
|
287
|
+
HeaderRedactionStrategy$1["PARTIAL"] = "partial";
|
|
288
|
+
/**
|
|
289
|
+
* Show only the last N characters, redact the rest
|
|
290
|
+
*/
|
|
291
|
+
HeaderRedactionStrategy$1["PARTIAL_END"] = "partial_end";
|
|
292
|
+
/**
|
|
293
|
+
* Remove the header entirely (same as deny list)
|
|
294
|
+
*/
|
|
295
|
+
HeaderRedactionStrategy$1["REMOVE"] = "remove";
|
|
296
|
+
return HeaderRedactionStrategy$1;
|
|
297
|
+
}({});
|
|
298
|
+
/**
|
|
299
|
+
* Default redaction configuration
|
|
300
|
+
*/
|
|
301
|
+
const DEFAULT_REDACTION_CONFIG = {
|
|
302
|
+
sensitivePatterns: DEFAULT_SENSITIVE_HEADER_PATTERNS,
|
|
303
|
+
strategy: HeaderRedactionStrategy.REPLACE,
|
|
304
|
+
redactionString: "[REDACTED]",
|
|
305
|
+
visibleChars: 4,
|
|
306
|
+
enabled: true
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Checks if a header name matches any sensitive pattern
|
|
310
|
+
* Uses case-insensitive matching with exact match, prefix/suffix, and substring matching
|
|
311
|
+
*
|
|
312
|
+
* @param headerName - The header name to check
|
|
313
|
+
* @param patterns - Array of patterns to match against (defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS)
|
|
314
|
+
* @returns true if the header matches any sensitive pattern
|
|
315
|
+
*/
|
|
316
|
+
function isSensitiveHeader(headerName, patterns = DEFAULT_SENSITIVE_HEADER_PATTERNS) {
|
|
317
|
+
if (!headerName || typeof headerName !== "string") return false;
|
|
318
|
+
if (!patterns || patterns.length === 0) return false;
|
|
319
|
+
const normalizedName = headerName.toLowerCase().trim();
|
|
320
|
+
if (normalizedName.length === 0) return false;
|
|
321
|
+
return patterns.some((pattern) => {
|
|
322
|
+
if (!pattern || typeof pattern !== "string") return false;
|
|
323
|
+
const normalizedPattern = pattern.toLowerCase().trim();
|
|
324
|
+
if (normalizedPattern.length === 0) return false;
|
|
325
|
+
if (normalizedName === normalizedPattern) return true;
|
|
326
|
+
if (normalizedName.includes(normalizedPattern)) return true;
|
|
327
|
+
if (normalizedPattern.includes(normalizedName)) return true;
|
|
328
|
+
return false;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Redacts a header value based on the configuration
|
|
333
|
+
*/
|
|
334
|
+
function redactHeaderValue(value, config) {
|
|
335
|
+
if (value === void 0 || value === null) return value;
|
|
336
|
+
if (Array.isArray(value)) return value.map((v) => redactSingleValue(v, config));
|
|
337
|
+
return redactSingleValue(value, config);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Redacts a single string value based on the configured strategy
|
|
341
|
+
*
|
|
342
|
+
* @param value - The value to redact
|
|
343
|
+
* @param config - Redaction configuration
|
|
344
|
+
* @returns Redacted value
|
|
345
|
+
*/
|
|
346
|
+
function redactSingleValue(value, config) {
|
|
347
|
+
if (!value || typeof value !== "string") return value;
|
|
348
|
+
const visibleChars = Math.max(0, Math.floor(config.visibleChars || 0));
|
|
349
|
+
const trimmedValue = value.trim();
|
|
350
|
+
if (trimmedValue.length === 0) return config.redactionString;
|
|
351
|
+
switch (config.strategy) {
|
|
352
|
+
case HeaderRedactionStrategy.REPLACE: return config.redactionString;
|
|
353
|
+
case HeaderRedactionStrategy.PARTIAL:
|
|
354
|
+
if (trimmedValue.length <= visibleChars) return config.redactionString;
|
|
355
|
+
return trimmedValue.substring(0, visibleChars) + config.redactionString;
|
|
356
|
+
case HeaderRedactionStrategy.PARTIAL_END:
|
|
357
|
+
if (trimmedValue.length <= visibleChars) return config.redactionString;
|
|
358
|
+
return config.redactionString + trimmedValue.substring(trimmedValue.length - visibleChars);
|
|
359
|
+
case HeaderRedactionStrategy.REMOVE: return config.redactionString;
|
|
360
|
+
default: return config.redactionString;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/filtering/header-filter.ts
|
|
366
|
+
/**
|
|
367
|
+
* Header filtering logic - applies allow/deny list rules and redaction
|
|
368
|
+
*/
|
|
369
|
+
const log = createLogger("[PingOps HeaderFilter]");
|
|
370
|
+
/**
|
|
371
|
+
* Normalizes header name to lowercase for case-insensitive matching
|
|
372
|
+
*/
|
|
373
|
+
function normalizeHeaderName(name) {
|
|
374
|
+
return name.toLowerCase();
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Merges redaction config with defaults
|
|
378
|
+
*/
|
|
379
|
+
function mergeRedactionConfig(config) {
|
|
380
|
+
if (!config) return DEFAULT_REDACTION_CONFIG;
|
|
381
|
+
if (config.enabled === false) return {
|
|
382
|
+
...DEFAULT_REDACTION_CONFIG,
|
|
383
|
+
enabled: false
|
|
384
|
+
};
|
|
385
|
+
return {
|
|
386
|
+
sensitivePatterns: config.sensitivePatterns ?? DEFAULT_REDACTION_CONFIG.sensitivePatterns,
|
|
387
|
+
strategy: config.strategy ?? DEFAULT_REDACTION_CONFIG.strategy,
|
|
388
|
+
redactionString: config.redactionString ?? DEFAULT_REDACTION_CONFIG.redactionString,
|
|
389
|
+
visibleChars: config.visibleChars ?? DEFAULT_REDACTION_CONFIG.visibleChars,
|
|
390
|
+
enabled: config.enabled ?? DEFAULT_REDACTION_CONFIG.enabled
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Filters headers based on allow/deny lists and applies redaction to sensitive headers
|
|
395
|
+
* - Deny list always wins (if header is in deny list, exclude it)
|
|
396
|
+
* - Allow list filters included headers (if specified, only include these)
|
|
397
|
+
* - Sensitive headers are redacted after filtering (if redaction is enabled)
|
|
398
|
+
* - Case-insensitive matching
|
|
399
|
+
*
|
|
400
|
+
* @param headers - Headers to filter
|
|
401
|
+
* @param headersAllowList - Optional allow list of header names to include
|
|
402
|
+
* @param headersDenyList - Optional deny list of header names to exclude
|
|
403
|
+
* @param redactionConfig - Optional configuration for header value redaction
|
|
404
|
+
* @returns Filtered and redacted headers
|
|
405
|
+
*/
|
|
406
|
+
function filterHeaders(headers, headersAllowList, headersDenyList, redactionConfig) {
|
|
407
|
+
const originalCount = Object.keys(headers).length;
|
|
408
|
+
const redaction = mergeRedactionConfig(redactionConfig);
|
|
409
|
+
log.debug("Filtering headers", {
|
|
410
|
+
originalHeaderCount: originalCount,
|
|
411
|
+
hasAllowList: !!headersAllowList && headersAllowList.length > 0,
|
|
412
|
+
hasDenyList: !!headersDenyList && headersDenyList.length > 0,
|
|
413
|
+
allowListCount: headersAllowList?.length || 0,
|
|
414
|
+
denyListCount: headersDenyList?.length || 0,
|
|
415
|
+
redactionEnabled: redaction.enabled,
|
|
416
|
+
redactionStrategy: redaction.strategy
|
|
417
|
+
});
|
|
418
|
+
const normalizedDenyList = headersDenyList?.map(normalizeHeaderName) ?? [];
|
|
419
|
+
const normalizedAllowList = headersAllowList?.map(normalizeHeaderName) ?? [];
|
|
420
|
+
const filtered = {};
|
|
421
|
+
const deniedHeaders = [];
|
|
422
|
+
const excludedHeaders = [];
|
|
423
|
+
const redactedHeaders = [];
|
|
424
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
425
|
+
const normalizedName = normalizeHeaderName(name);
|
|
426
|
+
if (normalizedDenyList.includes(normalizedName)) {
|
|
427
|
+
deniedHeaders.push(name);
|
|
428
|
+
log.debug("Header denied by deny list", { headerName: name });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (normalizedAllowList.length > 0) {
|
|
432
|
+
if (!normalizedAllowList.includes(normalizedName)) {
|
|
433
|
+
excludedHeaders.push(name);
|
|
434
|
+
log.debug("Header excluded (not in allow list)", { headerName: name });
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
let finalValue = value;
|
|
439
|
+
if (redaction.enabled) try {
|
|
440
|
+
if (isSensitiveHeader(name, redaction.sensitivePatterns)) {
|
|
441
|
+
if (redaction.strategy === HeaderRedactionStrategy.REMOVE) {
|
|
442
|
+
log.debug("Header removed by redaction strategy", { headerName: name });
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
finalValue = redactHeaderValue(value, redaction);
|
|
446
|
+
redactedHeaders.push(name);
|
|
447
|
+
log.debug("Header value redacted", {
|
|
448
|
+
headerName: name,
|
|
449
|
+
strategy: redaction.strategy
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
log.warn("Error redacting header value", {
|
|
454
|
+
headerName: name,
|
|
455
|
+
error: error instanceof Error ? error.message : String(error)
|
|
456
|
+
});
|
|
457
|
+
finalValue = value;
|
|
458
|
+
}
|
|
459
|
+
filtered[name] = finalValue;
|
|
460
|
+
}
|
|
461
|
+
const filteredCount = Object.keys(filtered).length;
|
|
462
|
+
log.info("Header filtering complete", {
|
|
463
|
+
originalCount,
|
|
464
|
+
filteredCount,
|
|
465
|
+
deniedCount: deniedHeaders.length,
|
|
466
|
+
excludedCount: excludedHeaders.length,
|
|
467
|
+
redactedCount: redactedHeaders.length,
|
|
468
|
+
deniedHeaders: deniedHeaders.length > 0 ? deniedHeaders : void 0,
|
|
469
|
+
excludedHeaders: excludedHeaders.length > 0 ? excludedHeaders : void 0,
|
|
470
|
+
redactedHeaders: redactedHeaders.length > 0 ? redactedHeaders : void 0
|
|
471
|
+
});
|
|
472
|
+
return filtered;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Extracts and normalizes headers from OpenTelemetry span attributes
|
|
476
|
+
*
|
|
477
|
+
* Handles two formats:
|
|
478
|
+
* 1. Flat array format (e.g., 'http.request.header.0', 'http.request.header.1')
|
|
479
|
+
* - 'http.request.header.0': 'Content-Type'
|
|
480
|
+
* - 'http.request.header.1': 'application/json'
|
|
481
|
+
* 2. Direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')
|
|
482
|
+
* - 'http.request.header.date': 'Mon, 12 Jan 2026 20:22:38 GMT'
|
|
483
|
+
* - 'http.request.header.content-type': 'application/json'
|
|
484
|
+
*
|
|
485
|
+
* This function converts them to:
|
|
486
|
+
* - { 'Content-Type': 'application/json', 'date': 'Mon, 12 Jan 2026 20:22:38 GMT' }
|
|
487
|
+
*/
|
|
488
|
+
function extractHeadersFromAttributes(attributes, headerPrefix) {
|
|
489
|
+
const headerMap = {};
|
|
490
|
+
const headerKeys = [];
|
|
491
|
+
const directKeyValueHeaders = [];
|
|
492
|
+
const prefixPattern = `${headerPrefix}.`;
|
|
493
|
+
const numericPattern = /* @__PURE__ */ new RegExp(`^${headerPrefix.replace(/\./g, "\\.")}\\.(\\d+)$`);
|
|
494
|
+
for (const key in attributes) if (key.startsWith(prefixPattern) && key !== headerPrefix) {
|
|
495
|
+
const numericMatch = key.match(numericPattern);
|
|
496
|
+
if (numericMatch) {
|
|
497
|
+
const index = parseInt(numericMatch[1], 10);
|
|
498
|
+
headerKeys.push(index);
|
|
499
|
+
} else {
|
|
500
|
+
const headerName = key.substring(prefixPattern.length);
|
|
501
|
+
if (headerName.length > 0) directKeyValueHeaders.push({
|
|
502
|
+
key,
|
|
503
|
+
headerName
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (headerKeys.length > 0) {
|
|
508
|
+
headerKeys.sort((a, b) => a - b);
|
|
509
|
+
for (let i = 0; i < headerKeys.length; i += 2) {
|
|
510
|
+
const nameIndex = headerKeys[i];
|
|
511
|
+
const valueIndex = headerKeys[i + 1];
|
|
512
|
+
if (valueIndex !== void 0) {
|
|
513
|
+
const nameKey = `${headerPrefix}.${nameIndex}`;
|
|
514
|
+
const valueKey = `${headerPrefix}.${valueIndex}`;
|
|
515
|
+
const headerName = attributes[nameKey];
|
|
516
|
+
const headerValue = attributes[valueKey];
|
|
517
|
+
if (headerName && headerValue !== void 0) {
|
|
518
|
+
const normalizedName = headerName.toLowerCase();
|
|
519
|
+
const existingKey = Object.keys(headerMap).find((k) => k.toLowerCase() === normalizedName);
|
|
520
|
+
if (existingKey) {
|
|
521
|
+
const existing = headerMap[existingKey];
|
|
522
|
+
headerMap[existingKey] = Array.isArray(existing) ? [...existing, headerValue] : [existing, headerValue];
|
|
523
|
+
} else headerMap[headerName] = headerValue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (directKeyValueHeaders.length > 0) for (const { key, headerName } of directKeyValueHeaders) {
|
|
529
|
+
const headerValue = attributes[key];
|
|
530
|
+
if (headerValue !== void 0 && headerValue !== null) {
|
|
531
|
+
const stringValue = typeof headerValue === "string" ? headerValue : String(headerValue);
|
|
532
|
+
const normalizedName = headerName.toLowerCase();
|
|
533
|
+
const existingKey = Object.keys(headerMap).find((k) => k.toLowerCase() === normalizedName);
|
|
534
|
+
if (existingKey) {
|
|
535
|
+
const existing = headerMap[existingKey];
|
|
536
|
+
headerMap[existingKey] = Array.isArray(existing) ? [...existing, stringValue] : [existing, stringValue];
|
|
537
|
+
} else headerMap[headerName] = stringValue;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return Object.keys(headerMap).length > 0 ? headerMap : null;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Type guard to check if value is a Headers-like object
|
|
544
|
+
*/
|
|
545
|
+
function isHeadersLike(headers) {
|
|
546
|
+
return typeof headers === "object" && headers !== null && "entries" in headers && typeof headers.entries === "function";
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Normalizes headers from various sources into a proper key-value object
|
|
550
|
+
*/
|
|
551
|
+
function normalizeHeaders(headers) {
|
|
552
|
+
const result = {};
|
|
553
|
+
if (!headers) return result;
|
|
554
|
+
try {
|
|
555
|
+
if (isHeadersLike(headers)) {
|
|
556
|
+
for (const [key, value] of headers.entries()) if (result[key]) {
|
|
557
|
+
const existing = result[key];
|
|
558
|
+
result[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
559
|
+
} else result[key] = value;
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
if (typeof headers === "object" && !Array.isArray(headers)) {
|
|
563
|
+
for (const [key, value] of Object.entries(headers)) if (!/^\d+$/.test(key)) result[key] = value;
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
if (Array.isArray(headers)) {
|
|
567
|
+
for (let i = 0; i < headers.length; i += 2) if (i + 1 < headers.length) {
|
|
568
|
+
const key = String(headers[i]);
|
|
569
|
+
result[key] = headers[i + 1];
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
} catch {}
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/utils/span-extractor.ts
|
|
579
|
+
/**
|
|
580
|
+
* Extracts domain from URL
|
|
581
|
+
*/
|
|
582
|
+
function extractDomainFromUrl(url) {
|
|
583
|
+
try {
|
|
584
|
+
return new URL(url).hostname;
|
|
585
|
+
} catch {
|
|
586
|
+
const match = url.match(/^(?:https?:\/\/)?([^/]+)/);
|
|
587
|
+
return match ? match[1] : "";
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Gets domain rule configuration for a given URL
|
|
592
|
+
*/
|
|
593
|
+
function getDomainRule(url, domainAllowList) {
|
|
594
|
+
if (!domainAllowList) return;
|
|
595
|
+
const domain = extractDomainFromUrl(url);
|
|
596
|
+
for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Determines if body should be captured based on priority:
|
|
600
|
+
* domain rule > global config > default (false)
|
|
601
|
+
*/
|
|
602
|
+
function shouldCaptureBody(domainRule, globalConfig, bodyType) {
|
|
603
|
+
if (domainRule) {
|
|
604
|
+
const domainValue = bodyType === "request" ? domainRule.captureRequestBody : domainRule.captureResponseBody;
|
|
605
|
+
if (domainValue !== void 0) return domainValue;
|
|
606
|
+
}
|
|
607
|
+
if (globalConfig !== void 0) return globalConfig;
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Extracts structured payload from a span
|
|
612
|
+
*/
|
|
613
|
+
function extractSpanPayload(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction) {
|
|
614
|
+
const attributes = span.attributes;
|
|
615
|
+
const url = attributes["http.url"] || attributes["url.full"];
|
|
616
|
+
const domainRule = url ? getDomainRule(url, domainAllowList) : void 0;
|
|
617
|
+
const headersAllowList = domainRule?.headersAllowList ?? globalHeadersAllowList;
|
|
618
|
+
const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;
|
|
619
|
+
const shouldCaptureReqBody = shouldCaptureBody(domainRule, globalCaptureRequestBody, "request");
|
|
620
|
+
const shouldCaptureRespBody = shouldCaptureBody(domainRule, globalCaptureResponseBody, "response");
|
|
621
|
+
let requestHeaders = {};
|
|
622
|
+
let responseHeaders = {};
|
|
623
|
+
const flatRequestHeaders = extractHeadersFromAttributes(attributes, "http.request.header");
|
|
624
|
+
const flatResponseHeaders = extractHeadersFromAttributes(attributes, "http.response.header");
|
|
625
|
+
const httpRequestHeadersValue = attributes["http.request.header"];
|
|
626
|
+
const httpResponseHeadersValue = attributes["http.response.header"];
|
|
627
|
+
const isHeadersRecord = (value) => {
|
|
628
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string" || Array.isArray(v) && v.every((item) => typeof item === "string") || v === void 0);
|
|
629
|
+
};
|
|
630
|
+
if (flatRequestHeaders) requestHeaders = filterHeaders(flatRequestHeaders, headersAllowList, headersDenyList, headerRedaction);
|
|
631
|
+
else if (isHeadersRecord(httpRequestHeadersValue)) requestHeaders = filterHeaders(httpRequestHeadersValue, headersAllowList, headersDenyList, headerRedaction);
|
|
632
|
+
if (flatResponseHeaders) responseHeaders = filterHeaders(flatResponseHeaders, headersAllowList, headersDenyList, headerRedaction);
|
|
633
|
+
else if (isHeadersRecord(httpResponseHeadersValue)) responseHeaders = filterHeaders(httpResponseHeadersValue, headersAllowList, headersDenyList, headerRedaction);
|
|
634
|
+
const extractedAttributes = { ...attributes };
|
|
635
|
+
for (const key in extractedAttributes) if (key.startsWith("http.request.header.") && key !== "http.request.header" || key.startsWith("http.response.header.") && key !== "http.response.header") delete extractedAttributes[key];
|
|
636
|
+
if (Object.keys(requestHeaders).length > 0) extractedAttributes["http.request.header"] = requestHeaders;
|
|
637
|
+
if (Object.keys(responseHeaders).length > 0) extractedAttributes["http.response.header"] = responseHeaders;
|
|
638
|
+
if (!shouldCaptureReqBody) delete extractedAttributes["http.request.body"];
|
|
639
|
+
if (!shouldCaptureRespBody) delete extractedAttributes["http.response.body"];
|
|
640
|
+
const spanContext = span.spanContext();
|
|
641
|
+
const parentSpanId = "parentSpanId" in span ? span.parentSpanId : void 0;
|
|
642
|
+
return {
|
|
643
|
+
traceId: spanContext.traceId,
|
|
644
|
+
spanId: spanContext.spanId,
|
|
645
|
+
parentSpanId,
|
|
646
|
+
name: span.name,
|
|
647
|
+
kind: span.kind.toString(),
|
|
648
|
+
startTime: (/* @__PURE__ */ new Date(span.startTime[0] * 1e3 + span.startTime[1] / 1e6)).toISOString(),
|
|
649
|
+
endTime: (/* @__PURE__ */ new Date(span.endTime[0] * 1e3 + span.endTime[1] / 1e6)).toISOString(),
|
|
650
|
+
duration: (span.endTime[0] - span.startTime[0]) * 1e3 + (span.endTime[1] - span.startTime[1]) / 1e6,
|
|
651
|
+
attributes: extractedAttributes,
|
|
652
|
+
status: {
|
|
653
|
+
code: span.status.code.toString(),
|
|
654
|
+
message: span.status.message
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/context-keys.ts
|
|
661
|
+
/**
|
|
662
|
+
* OpenTelemetry context keys for PingOps
|
|
663
|
+
*/
|
|
664
|
+
/**
|
|
665
|
+
* Context key for enabling HTTP instrumentation.
|
|
666
|
+
* When set to true, HTTP requests will be automatically instrumented.
|
|
667
|
+
* This allows wrapHttp to control which HTTP calls are captured.
|
|
668
|
+
*/
|
|
669
|
+
const PINGOPS_HTTP_ENABLED = (0, _opentelemetry_api.createContextKey)("pingops-http-enabled");
|
|
670
|
+
/**
|
|
671
|
+
* Context key for user ID attribute.
|
|
672
|
+
* Used to propagate user identifier to all spans in the context.
|
|
673
|
+
*/
|
|
674
|
+
const PINGOPS_USER_ID = (0, _opentelemetry_api.createContextKey)("pingops-user-id");
|
|
675
|
+
/**
|
|
676
|
+
* Context key for session ID attribute.
|
|
677
|
+
* Used to propagate session identifier to all spans in the context.
|
|
678
|
+
*/
|
|
679
|
+
const PINGOPS_SESSION_ID = (0, _opentelemetry_api.createContextKey)("pingops-session-id");
|
|
680
|
+
/**
|
|
681
|
+
* Context key for tags attribute.
|
|
682
|
+
* Used to propagate tags array to all spans in the context.
|
|
683
|
+
*/
|
|
684
|
+
const PINGOPS_TAGS = (0, _opentelemetry_api.createContextKey)("pingops-tags");
|
|
685
|
+
/**
|
|
686
|
+
* Context key for metadata attribute.
|
|
687
|
+
* Used to propagate metadata object to all spans in the context.
|
|
688
|
+
*/
|
|
689
|
+
const PINGOPS_METADATA = (0, _opentelemetry_api.createContextKey)("pingops-metadata");
|
|
690
|
+
/**
|
|
691
|
+
* Context key for capturing request body.
|
|
692
|
+
* When set, controls whether request bodies should be captured for HTTP spans.
|
|
693
|
+
* This allows wrapHttp to control body capture per-request.
|
|
694
|
+
*/
|
|
695
|
+
const PINGOPS_CAPTURE_REQUEST_BODY = (0, _opentelemetry_api.createContextKey)("pingops-capture-request-body");
|
|
696
|
+
/**
|
|
697
|
+
* Context key for capturing response body.
|
|
698
|
+
* When set, controls whether response bodies should be captured for HTTP spans.
|
|
699
|
+
* This allows wrapHttp to control body capture per-request.
|
|
700
|
+
*/
|
|
701
|
+
const PINGOPS_CAPTURE_RESPONSE_BODY = (0, _opentelemetry_api.createContextKey)("pingops-capture-response-body");
|
|
702
|
+
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region src/utils/context-extractor.ts
|
|
705
|
+
/**
|
|
706
|
+
* Extracts propagated attributes from the given context and returns them
|
|
707
|
+
* as span attributes that can be set on a span.
|
|
708
|
+
*
|
|
709
|
+
* @param parentContext - The OpenTelemetry context to extract attributes from
|
|
710
|
+
* @returns Record of attribute key-value pairs to set on spans
|
|
711
|
+
*/
|
|
712
|
+
function getPropagatedAttributesFromContext(parentContext) {
|
|
713
|
+
const attributes = {};
|
|
714
|
+
const userId = parentContext.getValue(PINGOPS_USER_ID);
|
|
715
|
+
if (userId !== void 0 && typeof userId === "string") attributes["pingops.user_id"] = userId;
|
|
716
|
+
const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);
|
|
717
|
+
if (sessionId !== void 0 && typeof sessionId === "string") attributes["pingops.session_id"] = sessionId;
|
|
718
|
+
const tags = parentContext.getValue(PINGOPS_TAGS);
|
|
719
|
+
if (tags !== void 0 && Array.isArray(tags)) attributes["pingops.tags"] = tags;
|
|
720
|
+
const metadata = parentContext.getValue(PINGOPS_METADATA);
|
|
721
|
+
if (metadata !== void 0 && typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
|
|
722
|
+
for (const [key, value] of Object.entries(metadata)) if (typeof value === "string") attributes[`pingops.metadata.${key}`] = value;
|
|
723
|
+
}
|
|
724
|
+
return attributes;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/wrap-http.ts
|
|
729
|
+
/**
|
|
730
|
+
* wrapHttp - Wraps a function to set attributes on HTTP spans created within the wrapped block.
|
|
731
|
+
*
|
|
732
|
+
* This function sets attributes (userId, sessionId, tags, metadata) in the OpenTelemetry
|
|
733
|
+
* context, which are automatically propagated to all spans created within the wrapped function.
|
|
734
|
+
*
|
|
735
|
+
* Instrumentation behavior:
|
|
736
|
+
* - If `initializePingops` was called: All HTTP requests are instrumented by default.
|
|
737
|
+
* `wrapHttp` only adds attributes to spans created within the wrapped block.
|
|
738
|
+
* - If `initializePingops` was NOT called: Only HTTP requests within `wrapHttp` blocks
|
|
739
|
+
* are instrumented. Requests outside `wrapHttp` are not instrumented.
|
|
740
|
+
*/
|
|
741
|
+
const logger = createLogger("[PingOps wrapHttp]");
|
|
742
|
+
/**
|
|
743
|
+
* Wraps a function to set attributes on HTTP spans created within the wrapped block.
|
|
744
|
+
*
|
|
745
|
+
* This function sets attributes (userId, sessionId, tags, metadata) in the OpenTelemetry
|
|
746
|
+
* context, which are automatically propagated to all spans created within the wrapped function.
|
|
747
|
+
*
|
|
748
|
+
* Instrumentation behavior:
|
|
749
|
+
* - If `initializePingops` was called: All HTTP requests are instrumented by default.
|
|
750
|
+
* `wrapHttp` only adds attributes to spans created within the wrapped block.
|
|
751
|
+
* - If `initializePingops` was NOT called: Only HTTP requests within `wrapHttp` blocks
|
|
752
|
+
* are instrumented. Requests outside `wrapHttp` are not instrumented.
|
|
753
|
+
*
|
|
754
|
+
* Note: This is the low-level API. For a simpler API with automatic setup,
|
|
755
|
+
* use `wrapHttp` from `@pingops/sdk` instead.
|
|
756
|
+
*
|
|
757
|
+
* @param options - Options including attributes and required callbacks
|
|
758
|
+
* @param fn - Function to execute within the attribute context
|
|
759
|
+
* @returns The result of the function
|
|
760
|
+
*/
|
|
761
|
+
function wrapHttp(options, fn) {
|
|
762
|
+
logger.debug("wrapHttp called", {
|
|
763
|
+
hasAttributes: !!options.attributes,
|
|
764
|
+
hasUserId: !!options.attributes?.userId,
|
|
765
|
+
hasSessionId: !!options.attributes?.sessionId,
|
|
766
|
+
hasTags: !!options.attributes?.tags,
|
|
767
|
+
hasMetadata: !!options.attributes?.metadata
|
|
768
|
+
});
|
|
769
|
+
const normalizedOptions = "checkInitialized" in options && "isGlobalInstrumentationEnabled" in options ? options : (() => {
|
|
770
|
+
throw new Error("wrapHttp requires checkInitialized and isGlobalInstrumentationEnabled callbacks. Use wrapHttp from @pingops/sdk for automatic setup.");
|
|
771
|
+
})();
|
|
772
|
+
const { checkInitialized, ensureInitialized } = normalizedOptions;
|
|
773
|
+
if (checkInitialized()) {
|
|
774
|
+
logger.debug("SDK already initialized, executing wrapHttp synchronously");
|
|
775
|
+
return executeWrapHttpWithContext(normalizedOptions, fn);
|
|
776
|
+
}
|
|
777
|
+
if (ensureInitialized) {
|
|
778
|
+
logger.debug("SDK not initialized, using provided ensureInitialized callback");
|
|
779
|
+
return ensureInitialized().then(() => {
|
|
780
|
+
logger.debug("SDK initialized, executing wrapHttp");
|
|
781
|
+
return executeWrapHttpWithContext(normalizedOptions, fn);
|
|
782
|
+
}).catch((error) => {
|
|
783
|
+
logger.error("Failed to initialize SDK for wrapHttp", { error: error instanceof Error ? error.message : String(error) });
|
|
784
|
+
throw error;
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
logger.debug("SDK not initialized and no ensureInitialized callback provided, executing wrapHttp");
|
|
788
|
+
return executeWrapHttpWithContext(normalizedOptions, fn);
|
|
789
|
+
}
|
|
790
|
+
function executeWrapHttpWithContext(options, fn) {
|
|
791
|
+
const { attributes, isGlobalInstrumentationEnabled } = options;
|
|
792
|
+
const globalInstrumentationEnabled = isGlobalInstrumentationEnabled();
|
|
793
|
+
logger.debug("Executing wrapHttp context", {
|
|
794
|
+
hasAttributes: !!attributes,
|
|
795
|
+
globalInstrumentationEnabled
|
|
796
|
+
});
|
|
797
|
+
let contextWithAttributes = _opentelemetry_api.context.active();
|
|
798
|
+
if (!globalInstrumentationEnabled) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_HTTP_ENABLED, true);
|
|
799
|
+
if (attributes) {
|
|
800
|
+
if (attributes.userId !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_USER_ID, attributes.userId);
|
|
801
|
+
if (attributes.sessionId !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_SESSION_ID, attributes.sessionId);
|
|
802
|
+
if (attributes.tags !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_TAGS, attributes.tags);
|
|
803
|
+
if (attributes.metadata !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_METADATA, attributes.metadata);
|
|
804
|
+
if (attributes.captureRequestBody !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_CAPTURE_REQUEST_BODY, attributes.captureRequestBody);
|
|
805
|
+
if (attributes.captureResponseBody !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_CAPTURE_RESPONSE_BODY, attributes.captureResponseBody);
|
|
806
|
+
}
|
|
807
|
+
return _opentelemetry_api.context.with(contextWithAttributes, () => {
|
|
808
|
+
try {
|
|
809
|
+
const result = fn();
|
|
810
|
+
if (result instanceof Promise) return result.catch((err) => {
|
|
811
|
+
logger.error("Error in wrapHttp async execution", { error: err instanceof Error ? err.message : String(err) });
|
|
812
|
+
throw err;
|
|
813
|
+
});
|
|
814
|
+
return result;
|
|
815
|
+
} catch (err) {
|
|
816
|
+
logger.error("Error in wrapHttp sync execution", { error: err instanceof Error ? err.message : String(err) });
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
//#endregion
|
|
823
|
+
exports.DEFAULT_REDACTION_CONFIG = DEFAULT_REDACTION_CONFIG;
|
|
824
|
+
exports.DEFAULT_SENSITIVE_HEADER_PATTERNS = DEFAULT_SENSITIVE_HEADER_PATTERNS;
|
|
825
|
+
exports.HeaderRedactionStrategy = HeaderRedactionStrategy;
|
|
826
|
+
exports.PINGOPS_CAPTURE_REQUEST_BODY = PINGOPS_CAPTURE_REQUEST_BODY;
|
|
827
|
+
exports.PINGOPS_CAPTURE_RESPONSE_BODY = PINGOPS_CAPTURE_RESPONSE_BODY;
|
|
828
|
+
exports.PINGOPS_HTTP_ENABLED = PINGOPS_HTTP_ENABLED;
|
|
829
|
+
exports.PINGOPS_METADATA = PINGOPS_METADATA;
|
|
830
|
+
exports.PINGOPS_SESSION_ID = PINGOPS_SESSION_ID;
|
|
831
|
+
exports.PINGOPS_TAGS = PINGOPS_TAGS;
|
|
832
|
+
exports.PINGOPS_USER_ID = PINGOPS_USER_ID;
|
|
833
|
+
exports.createLogger = createLogger;
|
|
834
|
+
exports.extractHeadersFromAttributes = extractHeadersFromAttributes;
|
|
835
|
+
exports.extractSpanPayload = extractSpanPayload;
|
|
836
|
+
exports.filterHeaders = filterHeaders;
|
|
837
|
+
exports.getPropagatedAttributesFromContext = getPropagatedAttributesFromContext;
|
|
838
|
+
exports.isSensitiveHeader = isSensitiveHeader;
|
|
839
|
+
exports.isSpanEligible = isSpanEligible;
|
|
840
|
+
exports.normalizeHeaders = normalizeHeaders;
|
|
841
|
+
exports.redactHeaderValue = redactHeaderValue;
|
|
842
|
+
exports.shouldCaptureSpan = shouldCaptureSpan;
|
|
843
|
+
exports.wrapHttp = wrapHttp;
|
|
844
|
+
//# sourceMappingURL=index.cjs.map
|