@pingops/core 0.2.0 → 0.2.1
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 +64 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +27 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +62 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,35 @@ function createLogger(prefix) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/utils/http-attributes.ts
|
|
33
|
+
/**
|
|
34
|
+
* Returns true when either legacy or modern HTTP method attribute is present.
|
|
35
|
+
*/
|
|
36
|
+
function hasHttpMethodAttribute(attributes) {
|
|
37
|
+
return attributes["http.method"] !== void 0 || attributes["http.request.method"] !== void 0;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns true when either legacy or modern HTTP URL attribute is present.
|
|
41
|
+
*/
|
|
42
|
+
function hasHttpUrlAttribute(attributes) {
|
|
43
|
+
return attributes["http.url"] !== void 0 || attributes["url.full"] !== void 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Extracts URL from known HTTP attributes with support for legacy + modern keys.
|
|
47
|
+
*
|
|
48
|
+
* If no explicit URL exists but server.address is available, falls back to a
|
|
49
|
+
* synthetic HTTPS URL for downstream domain filtering.
|
|
50
|
+
*/
|
|
51
|
+
function getHttpUrlFromAttributes(attributes) {
|
|
52
|
+
const legacyUrl = attributes["http.url"];
|
|
53
|
+
if (typeof legacyUrl === "string" && legacyUrl.length > 0) return legacyUrl;
|
|
54
|
+
const modernUrl = attributes["url.full"];
|
|
55
|
+
if (typeof modernUrl === "string" && modernUrl.length > 0) return modernUrl;
|
|
56
|
+
const serverAddress = attributes["server.address"];
|
|
57
|
+
if (typeof serverAddress === "string" && serverAddress.length > 0) return `https://${serverAddress}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
//#endregion
|
|
32
61
|
//#region src/filtering/span-filter.ts
|
|
33
62
|
/**
|
|
@@ -38,7 +67,10 @@ const log$2 = createLogger("[PingOps SpanFilter]");
|
|
|
38
67
|
* Checks if a span is eligible for capture based on span kind and attributes.
|
|
39
68
|
* A span is eligible if:
|
|
40
69
|
* 1. span.kind === SpanKind.CLIENT
|
|
41
|
-
* 2. AND has HTTP attributes
|
|
70
|
+
* 2. AND has HTTP attributes
|
|
71
|
+
* - method: http.method or http.request.method
|
|
72
|
+
* - url: http.url or url.full
|
|
73
|
+
* - host: server.address
|
|
42
74
|
* OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)
|
|
43
75
|
*/
|
|
44
76
|
function isSpanEligible(span) {
|
|
@@ -56,8 +88,8 @@ function isSpanEligible(span) {
|
|
|
56
88
|
return false;
|
|
57
89
|
}
|
|
58
90
|
const attributes = span.attributes;
|
|
59
|
-
const hasHttpMethod = attributes
|
|
60
|
-
const hasHttpUrl = attributes
|
|
91
|
+
const hasHttpMethod = hasHttpMethodAttribute(attributes);
|
|
92
|
+
const hasHttpUrl = hasHttpUrlAttribute(attributes);
|
|
61
93
|
const hasServerAddress = attributes["server.address"] !== void 0;
|
|
62
94
|
const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;
|
|
63
95
|
log$2.debug("Span eligibility check result", {
|
|
@@ -66,6 +98,10 @@ function isSpanEligible(span) {
|
|
|
66
98
|
httpAttributes: {
|
|
67
99
|
hasMethod: hasHttpMethod,
|
|
68
100
|
hasUrl: hasHttpUrl,
|
|
101
|
+
hasLegacyMethod: attributes["http.method"] !== void 0,
|
|
102
|
+
hasModernMethod: attributes["http.request.method"] !== void 0,
|
|
103
|
+
hasLegacyUrl: attributes["http.url"] !== void 0,
|
|
104
|
+
hasModernUrl: attributes["url.full"] !== void 0,
|
|
69
105
|
hasServerAddress
|
|
70
106
|
}
|
|
71
107
|
});
|
|
@@ -367,6 +403,10 @@ function redactSingleValue(value, config) {
|
|
|
367
403
|
* Header filtering logic - applies allow/deny list rules and redaction
|
|
368
404
|
*/
|
|
369
405
|
const log = createLogger("[PingOps HeaderFilter]");
|
|
406
|
+
function toHeaderString(value) {
|
|
407
|
+
if (typeof value === "string") return value;
|
|
408
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
409
|
+
}
|
|
370
410
|
/**
|
|
371
411
|
* Normalizes header name to lowercase for case-insensitive matching
|
|
372
412
|
*/
|
|
@@ -528,7 +568,8 @@ function extractHeadersFromAttributes(attributes, headerPrefix) {
|
|
|
528
568
|
if (directKeyValueHeaders.length > 0) for (const { key, headerName } of directKeyValueHeaders) {
|
|
529
569
|
const headerValue = attributes[key];
|
|
530
570
|
if (headerValue !== void 0 && headerValue !== null) {
|
|
531
|
-
const stringValue =
|
|
571
|
+
const stringValue = toHeaderString(headerValue);
|
|
572
|
+
if (stringValue === void 0) continue;
|
|
532
573
|
const normalizedName = headerName.toLowerCase();
|
|
533
574
|
const existingKey = Object.keys(headerMap).find((k) => k.toLowerCase() === normalizedName);
|
|
534
575
|
if (existingKey) {
|
|
@@ -552,6 +593,13 @@ function normalizeHeaders(headers) {
|
|
|
552
593
|
const result = {};
|
|
553
594
|
if (!headers) return result;
|
|
554
595
|
try {
|
|
596
|
+
if (Array.isArray(headers)) {
|
|
597
|
+
for (let i = 0; i < headers.length; i += 2) if (i + 1 < headers.length) {
|
|
598
|
+
const key = String(headers[i]);
|
|
599
|
+
result[key] = headers[i + 1];
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
555
603
|
if (isHeadersLike(headers)) {
|
|
556
604
|
for (const [key, value] of headers.entries()) if (result[key]) {
|
|
557
605
|
const existing = result[key];
|
|
@@ -563,13 +611,6 @@ function normalizeHeaders(headers) {
|
|
|
563
611
|
for (const [key, value] of Object.entries(headers)) if (!/^\d+$/.test(key)) result[key] = value;
|
|
564
612
|
return result;
|
|
565
613
|
}
|
|
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
614
|
} catch {}
|
|
574
615
|
return result;
|
|
575
616
|
}
|
|
@@ -590,9 +631,16 @@ const COMPRESSED_ENCODINGS = new Set([
|
|
|
590
631
|
"x-gzip",
|
|
591
632
|
"x-deflate"
|
|
592
633
|
]);
|
|
634
|
+
function safeStringify(value) {
|
|
635
|
+
if (typeof value === "string") return value;
|
|
636
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
637
|
+
}
|
|
593
638
|
function normalizeHeaderValue(v) {
|
|
594
639
|
if (v == null) return void 0;
|
|
595
|
-
|
|
640
|
+
if (Array.isArray(v)) return v.map((item) => safeStringify(item)).filter((item) => item !== void 0).join(", ").trim() || void 0;
|
|
641
|
+
const s = safeStringify(v);
|
|
642
|
+
if (!s) return void 0;
|
|
643
|
+
return s.trim() || void 0;
|
|
596
644
|
}
|
|
597
645
|
/**
|
|
598
646
|
* Returns true if the content-encoding header indicates a compressed body
|
|
@@ -652,7 +700,7 @@ function shouldCaptureBody(domainRule, globalConfig, bodyType) {
|
|
|
652
700
|
*/
|
|
653
701
|
function extractSpanPayload(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction) {
|
|
654
702
|
const attributes = span.attributes;
|
|
655
|
-
const url = attributes
|
|
703
|
+
const url = getHttpUrlFromAttributes(attributes);
|
|
656
704
|
const domainRule = url ? getDomainRule(url, domainAllowList) : void 0;
|
|
657
705
|
const headersAllowList = domainRule?.headersAllowList ?? globalHeadersAllowList;
|
|
658
706
|
const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;
|
|
@@ -806,7 +854,10 @@ exports.createTraceId = createTraceId;
|
|
|
806
854
|
exports.extractHeadersFromAttributes = extractHeadersFromAttributes;
|
|
807
855
|
exports.extractSpanPayload = extractSpanPayload;
|
|
808
856
|
exports.filterHeaders = filterHeaders;
|
|
857
|
+
exports.getHttpUrlFromAttributes = getHttpUrlFromAttributes;
|
|
809
858
|
exports.getPropagatedAttributesFromContext = getPropagatedAttributesFromContext;
|
|
859
|
+
exports.hasHttpMethodAttribute = hasHttpMethodAttribute;
|
|
860
|
+
exports.hasHttpUrlAttribute = hasHttpUrlAttribute;
|
|
810
861
|
exports.isCompressedContentEncoding = isCompressedContentEncoding;
|
|
811
862
|
exports.isSensitiveHeader = isSensitiveHeader;
|
|
812
863
|
exports.isSpanEligible = isSpanEligible;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["log","SpanKind","log"],"sources":["../src/logger.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/context-keys.ts","../src/utils/context-extractor.ts","../src/trace-id.ts"],"sourcesContent":["/**\n * Global logger utility for PingOps Core\n *\n * Provides consistent logging across all core components with support for\n * different log levels and debug mode control via PINGOPS_DEBUG environment variable.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\n/**\n * Creates a logger instance with a specific prefix\n *\n * @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')\n * @returns Logger instance\n */\nexport function createLogger(prefix: string): Logger {\n const isDebugEnabled = process.env.PINGOPS_DEBUG === \"true\";\n\n const formatMessage = (level: LogLevel, message: string): string => {\n const timestamp = new Date().toISOString();\n return `[${timestamp}] ${prefix} [${level.toUpperCase()}] ${message}`;\n };\n\n return {\n debug(message: string, ...args: unknown[]): void {\n if (isDebugEnabled) {\n console.debug(formatMessage(\"debug\", message), ...args);\n }\n },\n info(message: string, ...args: unknown[]): void {\n console.log(formatMessage(\"info\", message), ...args);\n },\n warn(message: string, ...args: unknown[]): void {\n console.warn(formatMessage(\"warn\", message), ...args);\n },\n error(message: string, ...args: unknown[]): void {\n console.error(formatMessage(\"error\", message), ...args);\n },\n };\n}\n","/**\n * Span filtering logic - determines if a span is eligible for capture\n */\n\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps SpanFilter]\");\n\n/**\n * Checks if a span is eligible for capture based on span kind and attributes.\n * A span is eligible if:\n * 1. span.kind === SpanKind.CLIENT\n * 2. AND has HTTP attributes (http.method, http.url, or server.address)\n * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)\n */\nexport function isSpanEligible(span: ReadableSpan): boolean {\n log.debug(\"Checking span eligibility\", {\n spanName: span.name,\n spanKind: span.kind,\n spanId: span.spanContext().spanId,\n traceId: span.spanContext().traceId,\n });\n\n // Must be a CLIENT span (outgoing request)\n if (span.kind !== SpanKind.CLIENT) {\n log.debug(\"Span not eligible: not CLIENT kind\", {\n spanName: span.name,\n spanKind: span.kind,\n });\n return false;\n }\n\n const attributes = span.attributes;\n\n // Check for HTTP attributes\n const hasHttpMethod = attributes[\"http.method\"] !== undefined;\n const hasHttpUrl = attributes[\"http.url\"] !== undefined;\n const hasServerAddress = attributes[\"server.address\"] !== undefined;\n\n const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;\n\n log.debug(\"Span eligibility check result\", {\n spanName: span.name,\n isEligible,\n httpAttributes: {\n hasMethod: hasHttpMethod,\n hasUrl: hasHttpUrl,\n hasServerAddress,\n },\n });\n\n return isEligible;\n}\n","/**\n * Domain filtering logic - applies allow/deny list rules\n */\n\nimport type { DomainRule } from \"../types\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps DomainFilter]\");\n\n/**\n * Extracts domain from a URL\n */\nfunction extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n const domain = urlObj.hostname;\n log.debug(\"Extracted domain from URL\", { url, domain });\n return domain;\n } catch {\n // If URL parsing fails, try to extract domain from string\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n const domain = match ? match[1] : \"\";\n log.debug(\"Extracted domain from URL (fallback)\", { url, domain });\n return domain;\n }\n}\n\n/**\n * Checks if a domain matches a rule (exact or suffix match)\n */\nfunction domainMatches(domain: string, ruleDomain: string): boolean {\n // Exact match\n if (domain === ruleDomain) {\n log.debug(\"Domain exact match\", { domain, ruleDomain });\n return true;\n }\n\n // Suffix match (e.g., .github.com matches api.github.com)\n if (ruleDomain.startsWith(\".\")) {\n const matches =\n domain.endsWith(ruleDomain) || domain === ruleDomain.slice(1);\n log.debug(\"Domain suffix match check\", { domain, ruleDomain, matches });\n return matches;\n }\n\n log.debug(\"Domain does not match\", { domain, ruleDomain });\n return false;\n}\n\n/**\n * Checks if a path matches any of the allowed paths (prefix match)\n */\nfunction pathMatches(path: string, allowedPaths?: string[]): boolean {\n if (!allowedPaths || allowedPaths.length === 0) {\n log.debug(\"No path restrictions, all paths match\", { path });\n return true; // No path restrictions means all paths match\n }\n\n const matches = allowedPaths.some((allowedPath) =>\n path.startsWith(allowedPath)\n );\n log.debug(\"Path match check\", { path, allowedPaths, matches });\n return matches;\n}\n\n/**\n * Determines if a span should be captured based on domain rules\n */\nexport function shouldCaptureSpan(\n url: string,\n domainAllowList?: DomainRule[],\n domainDenyList?: DomainRule[]\n): boolean {\n log.debug(\"Checking domain filter rules\", {\n url,\n hasAllowList: !!domainAllowList && domainAllowList.length > 0,\n hasDenyList: !!domainDenyList && domainDenyList.length > 0,\n allowListCount: domainAllowList?.length || 0,\n denyListCount: domainDenyList?.length || 0,\n });\n\n const domain = extractDomain(url);\n\n // Extract path from URL\n let path = \"/\";\n try {\n const urlObj = new URL(url);\n path = urlObj.pathname;\n } catch {\n // If URL parsing fails, try to extract path from string\n const pathMatch = url.match(/^(?:https?:\\/\\/)?[^/]+(\\/.*)?$/);\n path = pathMatch && pathMatch[1] ? pathMatch[1] : \"/\";\n }\n\n log.debug(\"Extracted domain and path\", { url, domain, path });\n\n // Deny list is evaluated first - if domain is denied, don't capture\n if (domainDenyList) {\n for (const rule of domainDenyList) {\n if (domainMatches(domain, rule.domain)) {\n log.info(\"Domain denied by deny list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return false;\n }\n }\n log.debug(\"Domain passed deny list check\", { domain });\n }\n\n // If no allow list, capture all (except denied)\n if (!domainAllowList || domainAllowList.length === 0) {\n log.debug(\"No allow list configured, capturing span\", { domain, url });\n return true;\n }\n\n // Check if domain matches any allow list rule\n for (const rule of domainAllowList) {\n if (domainMatches(domain, rule.domain)) {\n // If paths are specified, check path match\n if (rule.paths && rule.paths.length > 0) {\n const pathMatch = pathMatches(path, rule.paths);\n if (pathMatch) {\n log.info(\"Domain and path allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n url,\n });\n return true;\n } else {\n log.debug(\"Domain allowed but path not matched\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n });\n }\n } else {\n log.info(\"Domain allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return true;\n }\n }\n }\n\n // Domain not in allow list\n log.info(\"Domain not in allow list, filtering out\", { domain, url });\n return false;\n}\n","/**\n * Sensitive header patterns and redaction configuration\n */\n\n/**\n * Default patterns for sensitive headers that should be redacted\n * These are matched case-insensitively\n */\nexport const DEFAULT_SENSITIVE_HEADER_PATTERNS = [\n // Authentication & Authorization\n \"authorization\",\n \"www-authenticate\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"x-auth-token\",\n \"x-api-key\",\n \"x-api-token\",\n \"x-access-token\",\n \"x-auth-user\",\n \"x-auth-password\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n\n // API Keys & Access Tokens\n \"api-key\",\n \"apikey\",\n \"api_key\",\n \"access-key\",\n \"accesskey\",\n \"access_key\",\n \"secret-key\",\n \"secretkey\",\n \"secret_key\",\n \"private-key\",\n \"privatekey\",\n \"private_key\",\n\n // Session & Cookie tokens\n \"cookie\",\n \"set-cookie\",\n \"session-id\",\n \"sessionid\",\n \"session_id\",\n \"session-token\",\n \"sessiontoken\",\n \"session_token\",\n\n // OAuth & OAuth2\n \"oauth-token\",\n \"oauth_token\",\n \"oauth2-token\",\n \"oauth2_token\",\n \"bearer\",\n\n // AWS & Cloud credentials\n \"x-amz-security-token\",\n \"x-amz-signature\",\n \"x-aws-access-key\",\n \"x-aws-secret-key\",\n \"x-aws-session-token\",\n\n // Other common sensitive headers\n \"x-password\",\n \"x-secret\",\n \"x-token\",\n \"x-jwt\",\n \"x-jwt-token\",\n \"x-refresh-token\",\n \"x-client-secret\",\n \"x-client-id\",\n \"x-user-token\",\n \"x-service-key\",\n] as const;\n\n/**\n * Redaction strategies for sensitive header values\n */\nexport enum HeaderRedactionStrategy {\n /**\n * Replace the entire value with a fixed redaction string\n */\n REPLACE = \"replace\",\n /**\n * Show only the first N characters, redact the rest\n */\n PARTIAL = \"partial\",\n /**\n * Show only the last N characters, redact the rest\n */\n PARTIAL_END = \"partial_end\",\n /**\n * Remove the header entirely (same as deny list)\n */\n REMOVE = \"remove\",\n}\n\n/**\n * Configuration for header redaction\n */\nexport interface HeaderRedactionConfig {\n /**\n * Patterns to match sensitive headers (case-insensitive)\n * Defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS if not provided\n */\n sensitivePatterns?: readonly string[];\n\n /**\n * Redaction strategy to use\n * @default HeaderRedactionStrategy.REPLACE\n */\n strategy?: HeaderRedactionStrategy;\n\n /**\n * Redaction string used when strategy is REPLACE\n * @default \"[REDACTED]\"\n */\n redactionString?: string;\n\n /**\n * Number of characters to show when strategy is PARTIAL or PARTIAL_END\n * @default 4\n */\n visibleChars?: number;\n\n /**\n * Whether to enable redaction\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Default redaction configuration\n */\nexport const DEFAULT_REDACTION_CONFIG: Required<HeaderRedactionConfig> = {\n sensitivePatterns: DEFAULT_SENSITIVE_HEADER_PATTERNS,\n strategy: HeaderRedactionStrategy.REPLACE,\n redactionString: \"[REDACTED]\",\n visibleChars: 4,\n enabled: true,\n};\n\n/**\n * Checks if a header name matches any sensitive pattern\n * Uses case-insensitive matching with exact match, prefix/suffix, and substring matching\n *\n * @param headerName - The header name to check\n * @param patterns - Array of patterns to match against (defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS)\n * @returns true if the header matches any sensitive pattern\n */\nexport function isSensitiveHeader(\n headerName: string,\n patterns: readonly string[] = DEFAULT_SENSITIVE_HEADER_PATTERNS\n): boolean {\n if (!headerName || typeof headerName !== \"string\") {\n return false;\n }\n\n if (!patterns || patterns.length === 0) {\n return false;\n }\n\n const normalizedName = headerName.toLowerCase().trim();\n\n // Early return for empty string\n if (normalizedName.length === 0) {\n return false;\n }\n\n return patterns.some((pattern) => {\n if (!pattern || typeof pattern !== \"string\") {\n return false;\n }\n\n const normalizedPattern = pattern.toLowerCase().trim();\n\n // Empty pattern doesn't match\n if (normalizedPattern.length === 0) {\n return false;\n }\n\n // Exact match (most common case, check first)\n if (normalizedName === normalizedPattern) {\n return true;\n }\n\n // Check if header name contains the pattern (e.g., \"x-api-key\" contains \"api-key\")\n // This handles cases where patterns are embedded in header names\n if (normalizedName.includes(normalizedPattern)) {\n return true;\n }\n\n // Check if pattern contains the header name (for shorter patterns matching longer headers)\n // This is less common but handles edge cases\n if (normalizedPattern.includes(normalizedName)) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Redacts a header value based on the configuration\n */\nexport function redactHeaderValue(\n value: string | string[] | undefined,\n config: Required<HeaderRedactionConfig>\n): string | string[] | undefined {\n if (value === undefined || value === null) {\n return value;\n }\n\n // Handle array of values\n if (Array.isArray(value)) {\n return value.map((v) => redactSingleValue(v, config));\n }\n\n return redactSingleValue(value, config);\n}\n\n/**\n * Redacts a single string value based on the configured strategy\n *\n * @param value - The value to redact\n * @param config - Redaction configuration\n * @returns Redacted value\n */\nfunction redactSingleValue(\n value: string,\n config: Required<HeaderRedactionConfig>\n): string {\n // Validate input\n if (!value || typeof value !== \"string\") {\n return value;\n }\n\n // Ensure visibleChars is a positive integer\n const visibleChars = Math.max(0, Math.floor(config.visibleChars || 0));\n const trimmedValue = value.trim();\n\n // Handle empty or very short values\n if (trimmedValue.length === 0) {\n return config.redactionString;\n }\n\n switch (config.strategy) {\n case HeaderRedactionStrategy.REPLACE:\n return config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL:\n // Show first N characters, then redaction string\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return trimmedValue.substring(0, visibleChars) + config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL_END:\n // Show last N characters, with redaction string prefix\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return (\n config.redactionString +\n trimmedValue.substring(trimmedValue.length - visibleChars)\n );\n\n case HeaderRedactionStrategy.REMOVE:\n // This should be handled at the filter level, not here\n // But if we reach here, return redaction string as fallback\n return config.redactionString;\n\n default:\n // Unknown strategy - default to full redaction for safety\n return config.redactionString;\n }\n}\n","/**\n * Header filtering logic - applies allow/deny list rules and redaction\n */\n\nimport { createLogger } from \"../logger\";\nimport type { HeaderRedactionConfig } from \"./sensitive-headers\";\nimport {\n DEFAULT_REDACTION_CONFIG,\n isSensitiveHeader,\n redactHeaderValue,\n HeaderRedactionStrategy,\n} from \"./sensitive-headers\";\n\nconst log = createLogger(\"[PingOps HeaderFilter]\");\n\n/**\n * Normalizes header name to lowercase for case-insensitive matching\n */\nfunction normalizeHeaderName(name: string): string {\n return name.toLowerCase();\n}\n\n/**\n * Merges redaction config with defaults\n */\nfunction mergeRedactionConfig(\n config?: HeaderRedactionConfig\n): Required<HeaderRedactionConfig> {\n // If config is undefined, use default config (enabled by default)\n if (!config) {\n return DEFAULT_REDACTION_CONFIG;\n }\n\n // If explicitly disabled, return disabled config\n if (config.enabled === false) {\n return { ...DEFAULT_REDACTION_CONFIG, enabled: false };\n }\n\n // Otherwise, merge with defaults (enabled defaults to true)\n return {\n sensitivePatterns:\n config.sensitivePatterns ?? DEFAULT_REDACTION_CONFIG.sensitivePatterns,\n strategy: config.strategy ?? DEFAULT_REDACTION_CONFIG.strategy,\n redactionString:\n config.redactionString ?? DEFAULT_REDACTION_CONFIG.redactionString,\n visibleChars: config.visibleChars ?? DEFAULT_REDACTION_CONFIG.visibleChars,\n enabled: config.enabled ?? DEFAULT_REDACTION_CONFIG.enabled,\n };\n}\n\n/**\n * Filters headers based on allow/deny lists and applies redaction to sensitive headers\n * - Deny list always wins (if header is in deny list, exclude it)\n * - Allow list filters included headers (if specified, only include these)\n * - Sensitive headers are redacted after filtering (if redaction is enabled)\n * - Case-insensitive matching\n *\n * @param headers - Headers to filter\n * @param headersAllowList - Optional allow list of header names to include\n * @param headersDenyList - Optional deny list of header names to exclude\n * @param redactionConfig - Optional configuration for header value redaction\n * @returns Filtered and redacted headers\n */\nexport function filterHeaders(\n headers: Record<string, string | string[] | undefined>,\n headersAllowList?: string[],\n headersDenyList?: string[],\n redactionConfig?: HeaderRedactionConfig\n): Record<string, string | string[] | undefined> {\n const originalCount = Object.keys(headers).length;\n const redaction = mergeRedactionConfig(redactionConfig);\n\n log.debug(\"Filtering headers\", {\n originalHeaderCount: originalCount,\n hasAllowList: !!headersAllowList && headersAllowList.length > 0,\n hasDenyList: !!headersDenyList && headersDenyList.length > 0,\n allowListCount: headersAllowList?.length || 0,\n denyListCount: headersDenyList?.length || 0,\n redactionEnabled: redaction.enabled,\n redactionStrategy: redaction.strategy,\n });\n\n const normalizedDenyList = headersDenyList?.map(normalizeHeaderName) ?? [];\n const normalizedAllowList = headersAllowList?.map(normalizeHeaderName) ?? [];\n\n const filtered: Record<string, string | string[] | undefined> = {};\n const deniedHeaders: string[] = [];\n const excludedHeaders: string[] = [];\n const redactedHeaders: string[] = [];\n\n for (const [name, value] of Object.entries(headers)) {\n const normalizedName = normalizeHeaderName(name);\n\n // Deny list always wins\n if (normalizedDenyList.includes(normalizedName)) {\n deniedHeaders.push(name);\n log.debug(\"Header denied by deny list\", { headerName: name });\n continue;\n }\n\n // If allow list exists, only include headers in the list\n if (normalizedAllowList.length > 0) {\n if (!normalizedAllowList.includes(normalizedName)) {\n excludedHeaders.push(name);\n log.debug(\"Header excluded (not in allow list)\", { headerName: name });\n continue;\n }\n }\n\n // Apply redaction if enabled and header is sensitive\n let finalValue = value;\n if (redaction.enabled) {\n try {\n // Check if header matches sensitive patterns\n if (isSensitiveHeader(name, redaction.sensitivePatterns)) {\n // Handle REMOVE strategy at filter level\n if (redaction.strategy === HeaderRedactionStrategy.REMOVE) {\n log.debug(\"Header removed by redaction strategy\", {\n headerName: name,\n });\n continue;\n }\n\n // Redact the value\n finalValue = redactHeaderValue(value, redaction);\n redactedHeaders.push(name);\n log.debug(\"Header value redacted\", {\n headerName: name,\n strategy: redaction.strategy,\n });\n }\n } catch (error) {\n // Log error but don't fail - use original value as fallback\n log.warn(\"Error redacting header value\", {\n headerName: name,\n error: error instanceof Error ? error.message : String(error),\n });\n finalValue = value;\n }\n }\n\n filtered[name] = finalValue;\n }\n\n const filteredCount = Object.keys(filtered).length;\n log.info(\"Header filtering complete\", {\n originalCount,\n filteredCount,\n deniedCount: deniedHeaders.length,\n excludedCount: excludedHeaders.length,\n redactedCount: redactedHeaders.length,\n deniedHeaders: deniedHeaders.length > 0 ? deniedHeaders : undefined,\n excludedHeaders: excludedHeaders.length > 0 ? excludedHeaders : undefined,\n redactedHeaders: redactedHeaders.length > 0 ? redactedHeaders : undefined,\n });\n\n return filtered;\n}\n\n/**\n * Extracts and normalizes headers from OpenTelemetry span attributes\n *\n * Handles two formats:\n * 1. Flat array format (e.g., 'http.request.header.0', 'http.request.header.1')\n * - 'http.request.header.0': 'Content-Type'\n * - 'http.request.header.1': 'application/json'\n * 2. Direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n * - 'http.request.header.date': 'Mon, 12 Jan 2026 20:22:38 GMT'\n * - 'http.request.header.content-type': 'application/json'\n *\n * This function converts them to:\n * - { 'Content-Type': 'application/json', 'date': 'Mon, 12 Jan 2026 20:22:38 GMT' }\n */\nexport function extractHeadersFromAttributes(\n attributes: Record<string, unknown>,\n headerPrefix: \"http.request.header\" | \"http.response.header\"\n): Record<string, string | string[] | undefined> | null {\n const headerMap: Record<string, string | string[] | undefined> = {};\n const headerKeys: number[] = [];\n const directKeyValueHeaders: Array<{ key: string; headerName: string }> = [];\n\n const prefixPattern = `${headerPrefix}.`;\n const numericPattern = new RegExp(\n `^${headerPrefix.replace(/\\./g, \"\\\\.\")}\\\\.(\\\\d+)$`\n );\n\n // Find all keys matching the pattern\n for (const key in attributes) {\n if (key.startsWith(prefixPattern) && key !== headerPrefix) {\n // Check for numeric index format (flat array)\n const numericMatch = key.match(numericPattern);\n if (numericMatch) {\n const index = parseInt(numericMatch[1], 10);\n headerKeys.push(index);\n } else {\n // Check for direct key-value format (e.g., 'http.request.header.date')\n const headerName = key.substring(prefixPattern.length);\n if (headerName.length > 0) {\n directKeyValueHeaders.push({ key, headerName });\n }\n }\n }\n }\n\n // Process numeric index format (flat array)\n if (headerKeys.length > 0) {\n // Sort indices to process in order\n headerKeys.sort((a, b) => a - b);\n\n // Convert flat array to key-value pairs\n // Even indices are header names, odd indices are header values\n for (let i = 0; i < headerKeys.length; i += 2) {\n const nameIndex = headerKeys[i];\n const valueIndex = headerKeys[i + 1];\n\n if (valueIndex !== undefined) {\n const nameKey = `${headerPrefix}.${nameIndex}`;\n const valueKey = `${headerPrefix}.${valueIndex}`;\n\n const headerName = attributes[nameKey] as string | undefined;\n const headerValue = attributes[valueKey] as string | undefined;\n\n if (headerName && headerValue !== undefined) {\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, headerValue]\n : [existing as string, headerValue];\n } else {\n // Use original case for the first occurrence\n headerMap[headerName] = headerValue;\n }\n }\n }\n }\n }\n\n // Process direct key-value format (e.g., 'http.request.header.date')\n if (directKeyValueHeaders.length > 0) {\n for (const { key, headerName } of directKeyValueHeaders) {\n const headerValue = attributes[key];\n\n if (headerValue !== undefined && headerValue !== null) {\n // Convert to string if needed\n const stringValue =\n typeof headerValue === \"string\" ? headerValue : String(headerValue);\n\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, stringValue]\n : [existing as string, stringValue];\n } else {\n // Use the header name as stored (may be lowercase from instrumentation)\n headerMap[headerName] = stringValue;\n }\n }\n }\n }\n\n return Object.keys(headerMap).length > 0 ? headerMap : null;\n}\n\n/**\n * Type guard to check if value is a Headers-like object\n */\nfunction isHeadersLike(\n headers: unknown\n): headers is { entries: () => IterableIterator<[string, string]> } {\n return (\n typeof headers === \"object\" &&\n headers !== null &&\n \"entries\" in headers &&\n typeof (headers as { entries?: unknown }).entries === \"function\"\n );\n}\n\n/**\n * Normalizes headers from various sources into a proper key-value object\n */\nexport function normalizeHeaders(\n headers: unknown\n): Record<string, string | string[] | undefined> {\n const result: Record<string, string | string[] | undefined> = {};\n\n if (!headers) {\n return result;\n }\n\n try {\n // Handle Headers object (from fetch/undici)\n if (isHeadersLike(headers)) {\n for (const [key, value] of headers.entries()) {\n // Headers can have multiple values for the same key\n if (result[key]) {\n // Convert to array if not already\n const existing = result[key];\n result[key] = Array.isArray(existing)\n ? [...existing, value]\n : [existing, value];\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n // Handle plain object\n if (typeof headers === \"object\" && !Array.isArray(headers)) {\n for (const [key, value] of Object.entries(headers)) {\n // Skip numeric keys (array-like objects)\n if (!/^\\d+$/.test(key)) {\n result[key] = value as string | string[] | undefined;\n }\n }\n return result;\n }\n\n // Handle array (shouldn't happen, but handle gracefully)\n if (Array.isArray(headers)) {\n // Try to reconstruct from array pairs\n for (let i = 0; i < headers.length; i += 2) {\n if (i + 1 < headers.length) {\n const key = String(headers[i]);\n const value = headers[i + 1] as string | string[] | undefined;\n result[key] = value;\n }\n }\n return result;\n }\n } catch {\n // Fail silently - return empty object\n }\n\n return result;\n}\n","/**\n * Minimal body handling: buffer to string for span attributes.\n * No decompression or truncation; for compressed responses the instrumentation\n * sends base64 + content-encoding so the backend can decompress.\n */\n\n/** Span attribute for response content-encoding when body is sent as base64. */\nexport const HTTP_RESPONSE_CONTENT_ENCODING = \"http.response.content_encoding\";\n\nconst COMPRESSED_ENCODINGS = new Set([\n \"gzip\",\n \"br\",\n \"deflate\",\n \"x-gzip\",\n \"x-deflate\",\n]);\n\nfunction normalizeHeaderValue(v: unknown): string | undefined {\n if (v == null) return undefined;\n const s = Array.isArray(v) ? v.map(String).join(\", \") : String(v);\n return s.trim() || undefined;\n}\n\n/**\n * Returns true if the content-encoding header indicates a compressed body\n * (gzip, br, deflate, x-gzip, x-deflate). Used to decide whether to send\n * body as base64 + content-encoding for backend decompression.\n */\nexport function isCompressedContentEncoding(headerValue: unknown): boolean {\n const raw = normalizeHeaderValue(headerValue);\n if (!raw) return false;\n const first = raw.split(\",\")[0].trim().toLowerCase();\n return COMPRESSED_ENCODINGS.has(first);\n}\n\n/**\n * Converts a buffer to a UTF-8 string for use as request/response body on spans.\n * Returns null for null, undefined, or empty buffer.\n */\nexport function bufferToBodyString(\n buffer: Buffer | null | undefined\n): string | null {\n if (buffer == null || buffer.length === 0) return null;\n return buffer.toString(\"utf8\");\n}\n","/**\n * Extracts structured data from spans for PingOps backend\n */\n\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport type { DomainRule, SpanPayload } from \"../types\";\nimport type { HeaderRedactionConfig } from \"../filtering/sensitive-headers\";\nimport {\n filterHeaders,\n extractHeadersFromAttributes,\n} from \"../filtering/header-filter\";\n\n/**\n * Extracts domain from URL\n */\nfunction extractDomainFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n return match ? match[1] : \"\";\n }\n}\n\n/**\n * Gets domain rule configuration for a given URL\n */\nfunction getDomainRule(\n url: string,\n domainAllowList?: DomainRule[]\n): DomainRule | undefined {\n if (!domainAllowList) {\n return undefined;\n }\n\n const domain = extractDomainFromUrl(url);\n for (const rule of domainAllowList) {\n if (\n domain === rule.domain ||\n domain.endsWith(`.${rule.domain}`) ||\n domain === rule.domain.slice(1)\n ) {\n return rule;\n }\n }\n return undefined;\n}\n\n/**\n * Determines if body should be captured based on priority:\n * domain rule > global config > default (false)\n */\nfunction shouldCaptureBody(\n domainRule: DomainRule | undefined,\n globalConfig: boolean | undefined,\n bodyType: \"request\" | \"response\"\n): boolean {\n // Check domain-specific rule first\n if (domainRule) {\n const domainValue =\n bodyType === \"request\"\n ? domainRule.captureRequestBody\n : domainRule.captureResponseBody;\n if (domainValue !== undefined) {\n return domainValue;\n }\n }\n // Fall back to global config\n if (globalConfig !== undefined) {\n return globalConfig;\n }\n // Default to false\n return false;\n}\n\n/**\n * Extracts structured payload from a span\n */\nexport function extractSpanPayload(\n span: ReadableSpan,\n domainAllowList?: DomainRule[],\n globalHeadersAllowList?: string[],\n globalHeadersDenyList?: string[],\n globalCaptureRequestBody?: boolean,\n globalCaptureResponseBody?: boolean,\n headerRedaction?: HeaderRedactionConfig\n): SpanPayload | null {\n const attributes = span.attributes;\n const url =\n (attributes[\"http.url\"] as string) || (attributes[\"url.full\"] as string);\n\n // Get domain-specific rule if available\n const domainRule = url ? getDomainRule(url, domainAllowList) : undefined;\n\n // Merge global and domain-specific header rules\n const headersAllowList =\n domainRule?.headersAllowList ?? globalHeadersAllowList;\n const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;\n\n // Determine if bodies should be captured\n const shouldCaptureReqBody = shouldCaptureBody(\n domainRule,\n globalCaptureRequestBody,\n \"request\"\n );\n const shouldCaptureRespBody = shouldCaptureBody(\n domainRule,\n globalCaptureResponseBody,\n \"response\"\n );\n\n // Extract HTTP headers if available\n let requestHeaders: Record<string, string | string[] | undefined> = {};\n let responseHeaders: Record<string, string | string[] | undefined> = {};\n\n // First, try to extract flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1')\n // or direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n const flatRequestHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.request.header\"\n );\n const flatResponseHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.response.header\"\n );\n\n // Try to get headers from attributes (format may vary by instrumentation)\n const httpRequestHeadersValue = attributes[\"http.request.header\"];\n const httpResponseHeadersValue = attributes[\"http.response.header\"];\n\n // Type guard: check if value is a record/object with string keys\n const isHeadersRecord = (\n value: unknown\n ): value is Record<string, string | string[] | undefined> => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value).every(\n (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((item) => typeof item === \"string\")) ||\n v === undefined\n )\n );\n };\n\n // Use flat array format if available, otherwise use direct attribute\n if (flatRequestHeaders) {\n requestHeaders = filterHeaders(\n flatRequestHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpRequestHeadersValue)) {\n requestHeaders = filterHeaders(\n httpRequestHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n if (flatResponseHeaders) {\n responseHeaders = filterHeaders(\n flatResponseHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpResponseHeadersValue)) {\n responseHeaders = filterHeaders(\n httpResponseHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n // Build attributes object\n const extractedAttributes: Record<string, unknown> = {\n ...attributes,\n };\n\n // Remove flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1', etc.)\n // and direct key-value format headers (e.g., 'http.request.header.date', 'http.request.header.content-type')\n // We'll replace them with the proper key-value format\n for (const key in extractedAttributes) {\n if (\n (key.startsWith(\"http.request.header.\") &&\n key !== \"http.request.header\") ||\n (key.startsWith(\"http.response.header.\") &&\n key !== \"http.response.header\")\n ) {\n // Remove both numeric index format and direct key-value format\n delete extractedAttributes[key];\n }\n }\n\n // Add filtered headers in proper key-value format\n if (Object.keys(requestHeaders).length > 0) {\n extractedAttributes[\"http.request.header\"] = requestHeaders;\n }\n\n if (Object.keys(responseHeaders).length > 0) {\n extractedAttributes[\"http.response.header\"] = responseHeaders;\n }\n\n // Remove body attributes if capture is disabled\n if (!shouldCaptureReqBody) {\n delete extractedAttributes[\"http.request.body\"];\n }\n\n if (!shouldCaptureRespBody) {\n delete extractedAttributes[\"http.response.body\"];\n }\n\n // Build span payload\n const spanContext = span.spanContext();\n // parentSpanId may not be available in all versions of ReadableSpan\n const parentSpanId =\n \"parentSpanId\" in span\n ? (span as ReadableSpan & { parentSpanId?: string }).parentSpanId\n : undefined;\n return {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n parentSpanId,\n name: span.name,\n kind: span.kind.toString(),\n startTime: new Date(\n span.startTime[0] * 1000 + span.startTime[1] / 1000000\n ).toISOString(),\n endTime: new Date(\n span.endTime[0] * 1000 + span.endTime[1] / 1000000\n ).toISOString(),\n duration:\n (span.endTime[0] - span.startTime[0]) * 1000 +\n (span.endTime[1] - span.startTime[1]) / 1000000,\n attributes: extractedAttributes,\n status: {\n code: span.status.code.toString(),\n message: span.status.message,\n },\n };\n}\n","/**\n * OpenTelemetry context keys for PingOps\n */\n\nimport { createContextKey } from \"@opentelemetry/api\";\n\n/**\n * Context key for trace ID attribute.\n * Used to propagate trace identifier to all spans in the context.\n */\nexport const PINGOPS_TRACE_ID = createContextKey(\"pingops-trace-id\");\n\n/**\n * Context key for user ID attribute.\n * Used to propagate user identifier to all spans in the context.\n */\nexport const PINGOPS_USER_ID = createContextKey(\"pingops-user-id\");\n\n/**\n * Context key for session ID attribute.\n * Used to propagate session identifier to all spans in the context.\n */\nexport const PINGOPS_SESSION_ID = createContextKey(\"pingops-session-id\");\n\n/**\n * Context key for tags attribute.\n * Used to propagate tags array to all spans in the context.\n */\nexport const PINGOPS_TAGS = createContextKey(\"pingops-tags\");\n\n/**\n * Context key for metadata attribute.\n * Used to propagate metadata object to all spans in the context.\n */\nexport const PINGOPS_METADATA = createContextKey(\"pingops-metadata\");\n\n/**\n * Context key for capturing request body.\n * When set, controls whether request bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_REQUEST_BODY = createContextKey(\n \"pingops-capture-request-body\"\n);\n\n/**\n * Context key for capturing response body.\n * When set, controls whether response bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_RESPONSE_BODY = createContextKey(\n \"pingops-capture-response-body\"\n);\n","/**\n * Extracts propagated attributes from OpenTelemetry context\n */\n\nimport type { Context } from \"@opentelemetry/api\";\nimport {\n PINGOPS_TRACE_ID,\n PINGOPS_USER_ID,\n PINGOPS_SESSION_ID,\n PINGOPS_TAGS,\n PINGOPS_METADATA,\n} from \"../context-keys\";\n\n/**\n * Extracts propagated attributes from the given context and returns them\n * as span attributes that can be set on a span.\n *\n * @param parentContext - The OpenTelemetry context to extract attributes from\n * @returns Record of attribute key-value pairs to set on spans\n */\nexport function getPropagatedAttributesFromContext(\n parentContext: Context\n): Record<string, string | string[]> {\n const attributes: Record<string, string | string[]> = {};\n\n // Extract traceId\n const traceId = parentContext.getValue(PINGOPS_TRACE_ID);\n if (traceId !== undefined && typeof traceId === \"string\") {\n attributes[\"pingops.trace_id\"] = traceId;\n }\n\n // Extract userId\n const userId = parentContext.getValue(PINGOPS_USER_ID);\n if (userId !== undefined && typeof userId === \"string\") {\n attributes[\"pingops.user_id\"] = userId;\n }\n\n // Extract sessionId\n const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);\n if (sessionId !== undefined && typeof sessionId === \"string\") {\n attributes[\"pingops.session_id\"] = sessionId;\n }\n\n // Extract tags\n const tags = parentContext.getValue(PINGOPS_TAGS);\n if (tags !== undefined && Array.isArray(tags)) {\n attributes[\"pingops.tags\"] = tags;\n }\n\n // Extract metadata\n const metadata = parentContext.getValue(PINGOPS_METADATA);\n if (\n metadata !== undefined &&\n typeof metadata === \"object\" &&\n metadata !== null &&\n !Array.isArray(metadata)\n ) {\n // Flatten metadata object into span attributes with prefix\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n attributes[`pingops.metadata.${key}`] = value;\n }\n }\n }\n\n return attributes;\n}\n","/**\n * Deterministic and random trace ID generation for PingOps\n */\n\n/**\n * Converts a Uint8Array to a lowercase hex string.\n */\nexport function uint8ArrayToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Creates a trace ID (32 hex chars).\n * - If `seed` is provided: deterministic via SHA-256 of the seed (first 32 hex chars).\n * - Otherwise: random 16 bytes as 32 hex chars.\n */\nexport async function createTraceId(seed?: string): Promise<string> {\n if (seed) {\n const data = new TextEncoder().encode(seed);\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const hashArray = new Uint8Array(hashBuffer);\n return uint8ArrayToHex(hashArray).slice(0, 32);\n }\n\n const randomValues = crypto.getRandomValues(new Uint8Array(16));\n return uint8ArrayToHex(randomValues);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["log","SpanKind","log"],"sources":["../src/logger.ts","../src/utils/http-attributes.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/context-keys.ts","../src/utils/context-extractor.ts","../src/trace-id.ts"],"sourcesContent":["/**\n * Global logger utility for PingOps Core\n *\n * Provides consistent logging across all core components with support for\n * different log levels and debug mode control via PINGOPS_DEBUG environment variable.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\n/**\n * Creates a logger instance with a specific prefix\n *\n * @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')\n * @returns Logger instance\n */\nexport function createLogger(prefix: string): Logger {\n const isDebugEnabled = process.env.PINGOPS_DEBUG === \"true\";\n\n const formatMessage = (level: LogLevel, message: string): string => {\n const timestamp = new Date().toISOString();\n return `[${timestamp}] ${prefix} [${level.toUpperCase()}] ${message}`;\n };\n\n return {\n debug(message: string, ...args: unknown[]): void {\n if (isDebugEnabled) {\n console.debug(formatMessage(\"debug\", message), ...args);\n }\n },\n info(message: string, ...args: unknown[]): void {\n console.log(formatMessage(\"info\", message), ...args);\n },\n warn(message: string, ...args: unknown[]): void {\n console.warn(formatMessage(\"warn\", message), ...args);\n },\n error(message: string, ...args: unknown[]): void {\n console.error(formatMessage(\"error\", message), ...args);\n },\n };\n}\n","/**\n * Helpers for reading HTTP-related span attributes across legacy and modern\n * OpenTelemetry semantic conventions.\n */\n\ntype SpanAttributes = Record<string, unknown>;\n\n/**\n * Returns true when either legacy or modern HTTP method attribute is present.\n */\nexport function hasHttpMethodAttribute(attributes: SpanAttributes): boolean {\n return (\n attributes[\"http.method\"] !== undefined ||\n attributes[\"http.request.method\"] !== undefined\n );\n}\n\n/**\n * Returns true when either legacy or modern HTTP URL attribute is present.\n */\nexport function hasHttpUrlAttribute(attributes: SpanAttributes): boolean {\n return (\n attributes[\"http.url\"] !== undefined || attributes[\"url.full\"] !== undefined\n );\n}\n\n/**\n * Extracts URL from known HTTP attributes with support for legacy + modern keys.\n *\n * If no explicit URL exists but server.address is available, falls back to a\n * synthetic HTTPS URL for downstream domain filtering.\n */\nexport function getHttpUrlFromAttributes(\n attributes: SpanAttributes\n): string | undefined {\n const legacyUrl = attributes[\"http.url\"];\n if (typeof legacyUrl === \"string\" && legacyUrl.length > 0) {\n return legacyUrl;\n }\n\n const modernUrl = attributes[\"url.full\"];\n if (typeof modernUrl === \"string\" && modernUrl.length > 0) {\n return modernUrl;\n }\n\n const serverAddress = attributes[\"server.address\"];\n if (typeof serverAddress === \"string\" && serverAddress.length > 0) {\n return `https://${serverAddress}`;\n }\n\n return undefined;\n}\n","/**\n * Span filtering logic - determines if a span is eligible for capture\n */\n\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { createLogger } from \"../logger\";\nimport {\n hasHttpMethodAttribute,\n hasHttpUrlAttribute,\n} from \"../utils/http-attributes\";\n\nconst log = createLogger(\"[PingOps SpanFilter]\");\n\n/**\n * Checks if a span is eligible for capture based on span kind and attributes.\n * A span is eligible if:\n * 1. span.kind === SpanKind.CLIENT\n * 2. AND has HTTP attributes\n * - method: http.method or http.request.method\n * - url: http.url or url.full\n * - host: server.address\n * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)\n */\nexport function isSpanEligible(span: ReadableSpan): boolean {\n log.debug(\"Checking span eligibility\", {\n spanName: span.name,\n spanKind: span.kind,\n spanId: span.spanContext().spanId,\n traceId: span.spanContext().traceId,\n });\n\n // Must be a CLIENT span (outgoing request)\n if (span.kind !== SpanKind.CLIENT) {\n log.debug(\"Span not eligible: not CLIENT kind\", {\n spanName: span.name,\n spanKind: span.kind,\n });\n return false;\n }\n\n const attributes = span.attributes;\n\n // Check for HTTP attributes\n const hasHttpMethod = hasHttpMethodAttribute(attributes);\n const hasHttpUrl = hasHttpUrlAttribute(attributes);\n const hasServerAddress = attributes[\"server.address\"] !== undefined;\n\n const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;\n\n log.debug(\"Span eligibility check result\", {\n spanName: span.name,\n isEligible,\n httpAttributes: {\n hasMethod: hasHttpMethod,\n hasUrl: hasHttpUrl,\n hasLegacyMethod: attributes[\"http.method\"] !== undefined,\n hasModernMethod: attributes[\"http.request.method\"] !== undefined,\n hasLegacyUrl: attributes[\"http.url\"] !== undefined,\n hasModernUrl: attributes[\"url.full\"] !== undefined,\n hasServerAddress,\n },\n });\n\n return isEligible;\n}\n","/**\n * Domain filtering logic - applies allow/deny list rules\n */\n\nimport type { DomainRule } from \"../types\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps DomainFilter]\");\n\n/**\n * Extracts domain from a URL\n */\nfunction extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n const domain = urlObj.hostname;\n log.debug(\"Extracted domain from URL\", { url, domain });\n return domain;\n } catch {\n // If URL parsing fails, try to extract domain from string\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n const domain = match ? match[1] : \"\";\n log.debug(\"Extracted domain from URL (fallback)\", { url, domain });\n return domain;\n }\n}\n\n/**\n * Checks if a domain matches a rule (exact or suffix match)\n */\nfunction domainMatches(domain: string, ruleDomain: string): boolean {\n // Exact match\n if (domain === ruleDomain) {\n log.debug(\"Domain exact match\", { domain, ruleDomain });\n return true;\n }\n\n // Suffix match (e.g., .github.com matches api.github.com)\n if (ruleDomain.startsWith(\".\")) {\n const matches =\n domain.endsWith(ruleDomain) || domain === ruleDomain.slice(1);\n log.debug(\"Domain suffix match check\", { domain, ruleDomain, matches });\n return matches;\n }\n\n log.debug(\"Domain does not match\", { domain, ruleDomain });\n return false;\n}\n\n/**\n * Checks if a path matches any of the allowed paths (prefix match)\n */\nfunction pathMatches(path: string, allowedPaths?: string[]): boolean {\n if (!allowedPaths || allowedPaths.length === 0) {\n log.debug(\"No path restrictions, all paths match\", { path });\n return true; // No path restrictions means all paths match\n }\n\n const matches = allowedPaths.some((allowedPath) =>\n path.startsWith(allowedPath)\n );\n log.debug(\"Path match check\", { path, allowedPaths, matches });\n return matches;\n}\n\n/**\n * Determines if a span should be captured based on domain rules\n */\nexport function shouldCaptureSpan(\n url: string,\n domainAllowList?: DomainRule[],\n domainDenyList?: DomainRule[]\n): boolean {\n log.debug(\"Checking domain filter rules\", {\n url,\n hasAllowList: !!domainAllowList && domainAllowList.length > 0,\n hasDenyList: !!domainDenyList && domainDenyList.length > 0,\n allowListCount: domainAllowList?.length || 0,\n denyListCount: domainDenyList?.length || 0,\n });\n\n const domain = extractDomain(url);\n\n // Extract path from URL\n let path = \"/\";\n try {\n const urlObj = new URL(url);\n path = urlObj.pathname;\n } catch {\n // If URL parsing fails, try to extract path from string\n const pathMatch = url.match(/^(?:https?:\\/\\/)?[^/]+(\\/.*)?$/);\n path = pathMatch && pathMatch[1] ? pathMatch[1] : \"/\";\n }\n\n log.debug(\"Extracted domain and path\", { url, domain, path });\n\n // Deny list is evaluated first - if domain is denied, don't capture\n if (domainDenyList) {\n for (const rule of domainDenyList) {\n if (domainMatches(domain, rule.domain)) {\n log.info(\"Domain denied by deny list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return false;\n }\n }\n log.debug(\"Domain passed deny list check\", { domain });\n }\n\n // If no allow list, capture all (except denied)\n if (!domainAllowList || domainAllowList.length === 0) {\n log.debug(\"No allow list configured, capturing span\", { domain, url });\n return true;\n }\n\n // Check if domain matches any allow list rule\n for (const rule of domainAllowList) {\n if (domainMatches(domain, rule.domain)) {\n // If paths are specified, check path match\n if (rule.paths && rule.paths.length > 0) {\n const pathMatch = pathMatches(path, rule.paths);\n if (pathMatch) {\n log.info(\"Domain and path allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n url,\n });\n return true;\n } else {\n log.debug(\"Domain allowed but path not matched\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n });\n }\n } else {\n log.info(\"Domain allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return true;\n }\n }\n }\n\n // Domain not in allow list\n log.info(\"Domain not in allow list, filtering out\", { domain, url });\n return false;\n}\n","/**\n * Sensitive header patterns and redaction configuration\n */\n\n/**\n * Default patterns for sensitive headers that should be redacted\n * These are matched case-insensitively\n */\nexport const DEFAULT_SENSITIVE_HEADER_PATTERNS = [\n // Authentication & Authorization\n \"authorization\",\n \"www-authenticate\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"x-auth-token\",\n \"x-api-key\",\n \"x-api-token\",\n \"x-access-token\",\n \"x-auth-user\",\n \"x-auth-password\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n\n // API Keys & Access Tokens\n \"api-key\",\n \"apikey\",\n \"api_key\",\n \"access-key\",\n \"accesskey\",\n \"access_key\",\n \"secret-key\",\n \"secretkey\",\n \"secret_key\",\n \"private-key\",\n \"privatekey\",\n \"private_key\",\n\n // Session & Cookie tokens\n \"cookie\",\n \"set-cookie\",\n \"session-id\",\n \"sessionid\",\n \"session_id\",\n \"session-token\",\n \"sessiontoken\",\n \"session_token\",\n\n // OAuth & OAuth2\n \"oauth-token\",\n \"oauth_token\",\n \"oauth2-token\",\n \"oauth2_token\",\n \"bearer\",\n\n // AWS & Cloud credentials\n \"x-amz-security-token\",\n \"x-amz-signature\",\n \"x-aws-access-key\",\n \"x-aws-secret-key\",\n \"x-aws-session-token\",\n\n // Other common sensitive headers\n \"x-password\",\n \"x-secret\",\n \"x-token\",\n \"x-jwt\",\n \"x-jwt-token\",\n \"x-refresh-token\",\n \"x-client-secret\",\n \"x-client-id\",\n \"x-user-token\",\n \"x-service-key\",\n] as const;\n\n/**\n * Redaction strategies for sensitive header values\n */\nexport enum HeaderRedactionStrategy {\n /**\n * Replace the entire value with a fixed redaction string\n */\n REPLACE = \"replace\",\n /**\n * Show only the first N characters, redact the rest\n */\n PARTIAL = \"partial\",\n /**\n * Show only the last N characters, redact the rest\n */\n PARTIAL_END = \"partial_end\",\n /**\n * Remove the header entirely (same as deny list)\n */\n REMOVE = \"remove\",\n}\n\n/**\n * Configuration for header redaction\n */\nexport interface HeaderRedactionConfig {\n /**\n * Patterns to match sensitive headers (case-insensitive)\n * Defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS if not provided\n */\n sensitivePatterns?: readonly string[];\n\n /**\n * Redaction strategy to use\n * @default HeaderRedactionStrategy.REPLACE\n */\n strategy?: HeaderRedactionStrategy;\n\n /**\n * Redaction string used when strategy is REPLACE\n * @default \"[REDACTED]\"\n */\n redactionString?: string;\n\n /**\n * Number of characters to show when strategy is PARTIAL or PARTIAL_END\n * @default 4\n */\n visibleChars?: number;\n\n /**\n * Whether to enable redaction\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Default redaction configuration\n */\nexport const DEFAULT_REDACTION_CONFIG: Required<HeaderRedactionConfig> = {\n sensitivePatterns: DEFAULT_SENSITIVE_HEADER_PATTERNS,\n strategy: HeaderRedactionStrategy.REPLACE,\n redactionString: \"[REDACTED]\",\n visibleChars: 4,\n enabled: true,\n};\n\n/**\n * Checks if a header name matches any sensitive pattern\n * Uses case-insensitive matching with exact match, prefix/suffix, and substring matching\n *\n * @param headerName - The header name to check\n * @param patterns - Array of patterns to match against (defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS)\n * @returns true if the header matches any sensitive pattern\n */\nexport function isSensitiveHeader(\n headerName: string,\n patterns: readonly string[] = DEFAULT_SENSITIVE_HEADER_PATTERNS\n): boolean {\n if (!headerName || typeof headerName !== \"string\") {\n return false;\n }\n\n if (!patterns || patterns.length === 0) {\n return false;\n }\n\n const normalizedName = headerName.toLowerCase().trim();\n\n // Early return for empty string\n if (normalizedName.length === 0) {\n return false;\n }\n\n return patterns.some((pattern) => {\n if (!pattern || typeof pattern !== \"string\") {\n return false;\n }\n\n const normalizedPattern = pattern.toLowerCase().trim();\n\n // Empty pattern doesn't match\n if (normalizedPattern.length === 0) {\n return false;\n }\n\n // Exact match (most common case, check first)\n if (normalizedName === normalizedPattern) {\n return true;\n }\n\n // Check if header name contains the pattern (e.g., \"x-api-key\" contains \"api-key\")\n // This handles cases where patterns are embedded in header names\n if (normalizedName.includes(normalizedPattern)) {\n return true;\n }\n\n // Check if pattern contains the header name (for shorter patterns matching longer headers)\n // This is less common but handles edge cases\n if (normalizedPattern.includes(normalizedName)) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Redacts a header value based on the configuration\n */\nexport function redactHeaderValue(\n value: string | string[] | undefined,\n config: Required<HeaderRedactionConfig>\n): string | string[] | undefined {\n if (value === undefined || value === null) {\n return value;\n }\n\n // Handle array of values\n if (Array.isArray(value)) {\n return value.map((v) => redactSingleValue(v, config));\n }\n\n return redactSingleValue(value, config);\n}\n\n/**\n * Redacts a single string value based on the configured strategy\n *\n * @param value - The value to redact\n * @param config - Redaction configuration\n * @returns Redacted value\n */\nfunction redactSingleValue(\n value: string,\n config: Required<HeaderRedactionConfig>\n): string {\n // Validate input\n if (!value || typeof value !== \"string\") {\n return value;\n }\n\n // Ensure visibleChars is a positive integer\n const visibleChars = Math.max(0, Math.floor(config.visibleChars || 0));\n const trimmedValue = value.trim();\n\n // Handle empty or very short values\n if (trimmedValue.length === 0) {\n return config.redactionString;\n }\n\n switch (config.strategy) {\n case HeaderRedactionStrategy.REPLACE:\n return config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL:\n // Show first N characters, then redaction string\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return trimmedValue.substring(0, visibleChars) + config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL_END:\n // Show last N characters, with redaction string prefix\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return (\n config.redactionString +\n trimmedValue.substring(trimmedValue.length - visibleChars)\n );\n\n case HeaderRedactionStrategy.REMOVE:\n // This should be handled at the filter level, not here\n // But if we reach here, return redaction string as fallback\n return config.redactionString;\n\n default:\n // Unknown strategy - default to full redaction for safety\n return config.redactionString;\n }\n}\n","/**\n * Header filtering logic - applies allow/deny list rules and redaction\n */\n\nimport { createLogger } from \"../logger\";\nimport type { HeaderRedactionConfig } from \"./sensitive-headers\";\nimport {\n DEFAULT_REDACTION_CONFIG,\n isSensitiveHeader,\n redactHeaderValue,\n HeaderRedactionStrategy,\n} from \"./sensitive-headers\";\n\nconst log = createLogger(\"[PingOps HeaderFilter]\");\n\nfunction toHeaderString(value: unknown): string | undefined {\n if (typeof value === \"string\") return value;\n if (\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n return undefined;\n}\n\n/**\n * Normalizes header name to lowercase for case-insensitive matching\n */\nfunction normalizeHeaderName(name: string): string {\n return name.toLowerCase();\n}\n\n/**\n * Merges redaction config with defaults\n */\nfunction mergeRedactionConfig(\n config?: HeaderRedactionConfig\n): Required<HeaderRedactionConfig> {\n // If config is undefined, use default config (enabled by default)\n if (!config) {\n return DEFAULT_REDACTION_CONFIG;\n }\n\n // If explicitly disabled, return disabled config\n if (config.enabled === false) {\n return { ...DEFAULT_REDACTION_CONFIG, enabled: false };\n }\n\n // Otherwise, merge with defaults (enabled defaults to true)\n return {\n sensitivePatterns:\n config.sensitivePatterns ?? DEFAULT_REDACTION_CONFIG.sensitivePatterns,\n strategy: config.strategy ?? DEFAULT_REDACTION_CONFIG.strategy,\n redactionString:\n config.redactionString ?? DEFAULT_REDACTION_CONFIG.redactionString,\n visibleChars: config.visibleChars ?? DEFAULT_REDACTION_CONFIG.visibleChars,\n enabled: config.enabled ?? DEFAULT_REDACTION_CONFIG.enabled,\n };\n}\n\n/**\n * Filters headers based on allow/deny lists and applies redaction to sensitive headers\n * - Deny list always wins (if header is in deny list, exclude it)\n * - Allow list filters included headers (if specified, only include these)\n * - Sensitive headers are redacted after filtering (if redaction is enabled)\n * - Case-insensitive matching\n *\n * @param headers - Headers to filter\n * @param headersAllowList - Optional allow list of header names to include\n * @param headersDenyList - Optional deny list of header names to exclude\n * @param redactionConfig - Optional configuration for header value redaction\n * @returns Filtered and redacted headers\n */\nexport function filterHeaders(\n headers: Record<string, string | string[] | undefined>,\n headersAllowList?: string[],\n headersDenyList?: string[],\n redactionConfig?: HeaderRedactionConfig\n): Record<string, string | string[] | undefined> {\n const originalCount = Object.keys(headers).length;\n const redaction = mergeRedactionConfig(redactionConfig);\n\n log.debug(\"Filtering headers\", {\n originalHeaderCount: originalCount,\n hasAllowList: !!headersAllowList && headersAllowList.length > 0,\n hasDenyList: !!headersDenyList && headersDenyList.length > 0,\n allowListCount: headersAllowList?.length || 0,\n denyListCount: headersDenyList?.length || 0,\n redactionEnabled: redaction.enabled,\n redactionStrategy: redaction.strategy,\n });\n\n const normalizedDenyList = headersDenyList?.map(normalizeHeaderName) ?? [];\n const normalizedAllowList = headersAllowList?.map(normalizeHeaderName) ?? [];\n\n const filtered: Record<string, string | string[] | undefined> = {};\n const deniedHeaders: string[] = [];\n const excludedHeaders: string[] = [];\n const redactedHeaders: string[] = [];\n\n for (const [name, value] of Object.entries(headers)) {\n const normalizedName = normalizeHeaderName(name);\n\n // Deny list always wins\n if (normalizedDenyList.includes(normalizedName)) {\n deniedHeaders.push(name);\n log.debug(\"Header denied by deny list\", { headerName: name });\n continue;\n }\n\n // If allow list exists, only include headers in the list\n if (normalizedAllowList.length > 0) {\n if (!normalizedAllowList.includes(normalizedName)) {\n excludedHeaders.push(name);\n log.debug(\"Header excluded (not in allow list)\", { headerName: name });\n continue;\n }\n }\n\n // Apply redaction if enabled and header is sensitive\n let finalValue = value;\n if (redaction.enabled) {\n try {\n // Check if header matches sensitive patterns\n if (isSensitiveHeader(name, redaction.sensitivePatterns)) {\n // Handle REMOVE strategy at filter level\n if (redaction.strategy === HeaderRedactionStrategy.REMOVE) {\n log.debug(\"Header removed by redaction strategy\", {\n headerName: name,\n });\n continue;\n }\n\n // Redact the value\n finalValue = redactHeaderValue(value, redaction);\n redactedHeaders.push(name);\n log.debug(\"Header value redacted\", {\n headerName: name,\n strategy: redaction.strategy,\n });\n }\n } catch (error) {\n // Log error but don't fail - use original value as fallback\n log.warn(\"Error redacting header value\", {\n headerName: name,\n error: error instanceof Error ? error.message : String(error),\n });\n finalValue = value;\n }\n }\n\n filtered[name] = finalValue;\n }\n\n const filteredCount = Object.keys(filtered).length;\n log.info(\"Header filtering complete\", {\n originalCount,\n filteredCount,\n deniedCount: deniedHeaders.length,\n excludedCount: excludedHeaders.length,\n redactedCount: redactedHeaders.length,\n deniedHeaders: deniedHeaders.length > 0 ? deniedHeaders : undefined,\n excludedHeaders: excludedHeaders.length > 0 ? excludedHeaders : undefined,\n redactedHeaders: redactedHeaders.length > 0 ? redactedHeaders : undefined,\n });\n\n return filtered;\n}\n\n/**\n * Extracts and normalizes headers from OpenTelemetry span attributes\n *\n * Handles two formats:\n * 1. Flat array format (e.g., 'http.request.header.0', 'http.request.header.1')\n * - 'http.request.header.0': 'Content-Type'\n * - 'http.request.header.1': 'application/json'\n * 2. Direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n * - 'http.request.header.date': 'Mon, 12 Jan 2026 20:22:38 GMT'\n * - 'http.request.header.content-type': 'application/json'\n *\n * This function converts them to:\n * - { 'Content-Type': 'application/json', 'date': 'Mon, 12 Jan 2026 20:22:38 GMT' }\n */\nexport function extractHeadersFromAttributes(\n attributes: Record<string, unknown>,\n headerPrefix: \"http.request.header\" | \"http.response.header\"\n): Record<string, string | string[] | undefined> | null {\n const headerMap: Record<string, string | string[] | undefined> = {};\n const headerKeys: number[] = [];\n const directKeyValueHeaders: Array<{ key: string; headerName: string }> = [];\n\n const prefixPattern = `${headerPrefix}.`;\n const numericPattern = new RegExp(\n `^${headerPrefix.replace(/\\./g, \"\\\\.\")}\\\\.(\\\\d+)$`\n );\n\n // Find all keys matching the pattern\n for (const key in attributes) {\n if (key.startsWith(prefixPattern) && key !== headerPrefix) {\n // Check for numeric index format (flat array)\n const numericMatch = key.match(numericPattern);\n if (numericMatch) {\n const index = parseInt(numericMatch[1], 10);\n headerKeys.push(index);\n } else {\n // Check for direct key-value format (e.g., 'http.request.header.date')\n const headerName = key.substring(prefixPattern.length);\n if (headerName.length > 0) {\n directKeyValueHeaders.push({ key, headerName });\n }\n }\n }\n }\n\n // Process numeric index format (flat array)\n if (headerKeys.length > 0) {\n // Sort indices to process in order\n headerKeys.sort((a, b) => a - b);\n\n // Convert flat array to key-value pairs\n // Even indices are header names, odd indices are header values\n for (let i = 0; i < headerKeys.length; i += 2) {\n const nameIndex = headerKeys[i];\n const valueIndex = headerKeys[i + 1];\n\n if (valueIndex !== undefined) {\n const nameKey = `${headerPrefix}.${nameIndex}`;\n const valueKey = `${headerPrefix}.${valueIndex}`;\n\n const headerName = attributes[nameKey] as string | undefined;\n const headerValue = attributes[valueKey] as string | undefined;\n\n if (headerName && headerValue !== undefined) {\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, headerValue]\n : [existing as string, headerValue];\n } else {\n // Use original case for the first occurrence\n headerMap[headerName] = headerValue;\n }\n }\n }\n }\n }\n\n // Process direct key-value format (e.g., 'http.request.header.date')\n if (directKeyValueHeaders.length > 0) {\n for (const { key, headerName } of directKeyValueHeaders) {\n const headerValue = attributes[key];\n\n if (headerValue !== undefined && headerValue !== null) {\n // Convert to string if needed\n const stringValue = toHeaderString(headerValue);\n if (stringValue === undefined) {\n continue;\n }\n\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, stringValue]\n : [existing as string, stringValue];\n } else {\n // Use the header name as stored (may be lowercase from instrumentation)\n headerMap[headerName] = stringValue;\n }\n }\n }\n }\n\n return Object.keys(headerMap).length > 0 ? headerMap : null;\n}\n\n/**\n * Type guard to check if value is a Headers-like object\n */\nfunction isHeadersLike(\n headers: unknown\n): headers is { entries: () => IterableIterator<[string, string]> } {\n return (\n typeof headers === \"object\" &&\n headers !== null &&\n \"entries\" in headers &&\n typeof (headers as { entries?: unknown }).entries === \"function\"\n );\n}\n\n/**\n * Normalizes headers from various sources into a proper key-value object\n */\nexport function normalizeHeaders(\n headers: unknown\n): Record<string, string | string[] | undefined> {\n const result: Record<string, string | string[] | undefined> = {};\n\n if (!headers) {\n return result;\n }\n\n try {\n // Handle array first: arrays also expose entries(), so this must run\n // before the Headers-like branch.\n if (Array.isArray(headers)) {\n // Try to reconstruct from array pairs\n for (let i = 0; i < headers.length; i += 2) {\n if (i + 1 < headers.length) {\n const key = String(headers[i]);\n const value = headers[i + 1] as string | string[] | undefined;\n result[key] = value;\n }\n }\n return result;\n }\n\n // Handle Headers object (from fetch/undici)\n if (isHeadersLike(headers)) {\n for (const [key, value] of headers.entries()) {\n // Headers can have multiple values for the same key\n if (result[key]) {\n // Convert to array if not already\n const existing = result[key];\n result[key] = Array.isArray(existing)\n ? [...existing, value]\n : [existing, value];\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n // Handle plain object\n if (typeof headers === \"object\" && !Array.isArray(headers)) {\n for (const [key, value] of Object.entries(headers)) {\n // Skip numeric keys (array-like objects)\n if (!/^\\d+$/.test(key)) {\n result[key] = value as string | string[] | undefined;\n }\n }\n return result;\n }\n } catch {\n // Fail silently - return empty object\n }\n\n return result;\n}\n","/**\n * Minimal body handling: buffer to string for span attributes.\n * No decompression or truncation; for compressed responses the instrumentation\n * sends base64 + content-encoding so the backend can decompress.\n */\n\n/** Span attribute for response content-encoding when body is sent as base64. */\nexport const HTTP_RESPONSE_CONTENT_ENCODING = \"http.response.content_encoding\";\n\nconst COMPRESSED_ENCODINGS = new Set([\n \"gzip\",\n \"br\",\n \"deflate\",\n \"x-gzip\",\n \"x-deflate\",\n]);\n\nfunction safeStringify(value: unknown): string | undefined {\n if (typeof value === \"string\") return value;\n if (\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n return undefined;\n}\n\nfunction normalizeHeaderValue(v: unknown): string | undefined {\n if (v == null) return undefined;\n if (Array.isArray(v)) {\n const parts = v\n .map((item) => safeStringify(item))\n .filter((item): item is string => item !== undefined);\n const joined = parts.join(\", \").trim();\n return joined || undefined;\n }\n\n const s = safeStringify(v);\n if (!s) return undefined;\n return s.trim() || undefined;\n}\n\n/**\n * Returns true if the content-encoding header indicates a compressed body\n * (gzip, br, deflate, x-gzip, x-deflate). Used to decide whether to send\n * body as base64 + content-encoding for backend decompression.\n */\nexport function isCompressedContentEncoding(headerValue: unknown): boolean {\n const raw = normalizeHeaderValue(headerValue);\n if (!raw) return false;\n const first = raw.split(\",\")[0].trim().toLowerCase();\n return COMPRESSED_ENCODINGS.has(first);\n}\n\n/**\n * Converts a buffer to a UTF-8 string for use as request/response body on spans.\n * Returns null for null, undefined, or empty buffer.\n */\nexport function bufferToBodyString(\n buffer: Buffer | null | undefined\n): string | null {\n if (buffer == null || buffer.length === 0) return null;\n return buffer.toString(\"utf8\");\n}\n","/**\n * Extracts structured data from spans for PingOps backend\n */\n\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport type { DomainRule, SpanPayload } from \"../types\";\nimport type { HeaderRedactionConfig } from \"../filtering/sensitive-headers\";\nimport { getHttpUrlFromAttributes } from \"./http-attributes\";\nimport {\n filterHeaders,\n extractHeadersFromAttributes,\n} from \"../filtering/header-filter\";\n\n/**\n * Extracts domain from URL\n */\nfunction extractDomainFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n return match ? match[1] : \"\";\n }\n}\n\n/**\n * Gets domain rule configuration for a given URL\n */\nfunction getDomainRule(\n url: string,\n domainAllowList?: DomainRule[]\n): DomainRule | undefined {\n if (!domainAllowList) {\n return undefined;\n }\n\n const domain = extractDomainFromUrl(url);\n for (const rule of domainAllowList) {\n if (\n domain === rule.domain ||\n domain.endsWith(`.${rule.domain}`) ||\n domain === rule.domain.slice(1)\n ) {\n return rule;\n }\n }\n return undefined;\n}\n\n/**\n * Determines if body should be captured based on priority:\n * domain rule > global config > default (false)\n */\nfunction shouldCaptureBody(\n domainRule: DomainRule | undefined,\n globalConfig: boolean | undefined,\n bodyType: \"request\" | \"response\"\n): boolean {\n // Check domain-specific rule first\n if (domainRule) {\n const domainValue =\n bodyType === \"request\"\n ? domainRule.captureRequestBody\n : domainRule.captureResponseBody;\n if (domainValue !== undefined) {\n return domainValue;\n }\n }\n // Fall back to global config\n if (globalConfig !== undefined) {\n return globalConfig;\n }\n // Default to false\n return false;\n}\n\n/**\n * Extracts structured payload from a span\n */\nexport function extractSpanPayload(\n span: ReadableSpan,\n domainAllowList?: DomainRule[],\n globalHeadersAllowList?: string[],\n globalHeadersDenyList?: string[],\n globalCaptureRequestBody?: boolean,\n globalCaptureResponseBody?: boolean,\n headerRedaction?: HeaderRedactionConfig\n): SpanPayload | null {\n const attributes = span.attributes;\n const url = getHttpUrlFromAttributes(attributes);\n\n // Get domain-specific rule if available\n const domainRule = url ? getDomainRule(url, domainAllowList) : undefined;\n\n // Merge global and domain-specific header rules\n const headersAllowList =\n domainRule?.headersAllowList ?? globalHeadersAllowList;\n const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;\n\n // Determine if bodies should be captured\n const shouldCaptureReqBody = shouldCaptureBody(\n domainRule,\n globalCaptureRequestBody,\n \"request\"\n );\n const shouldCaptureRespBody = shouldCaptureBody(\n domainRule,\n globalCaptureResponseBody,\n \"response\"\n );\n\n // Extract HTTP headers if available\n let requestHeaders: Record<string, string | string[] | undefined> = {};\n let responseHeaders: Record<string, string | string[] | undefined> = {};\n\n // First, try to extract flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1')\n // or direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n const flatRequestHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.request.header\"\n );\n const flatResponseHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.response.header\"\n );\n\n // Try to get headers from attributes (format may vary by instrumentation)\n const httpRequestHeadersValue = attributes[\"http.request.header\"];\n const httpResponseHeadersValue = attributes[\"http.response.header\"];\n\n // Type guard: check if value is a record/object with string keys\n const isHeadersRecord = (\n value: unknown\n ): value is Record<string, string | string[] | undefined> => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value).every(\n (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((item) => typeof item === \"string\")) ||\n v === undefined\n )\n );\n };\n\n // Use flat array format if available, otherwise use direct attribute\n if (flatRequestHeaders) {\n requestHeaders = filterHeaders(\n flatRequestHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpRequestHeadersValue)) {\n requestHeaders = filterHeaders(\n httpRequestHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n if (flatResponseHeaders) {\n responseHeaders = filterHeaders(\n flatResponseHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpResponseHeadersValue)) {\n responseHeaders = filterHeaders(\n httpResponseHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n // Build attributes object\n const extractedAttributes: Record<string, unknown> = {\n ...attributes,\n };\n\n // Remove flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1', etc.)\n // and direct key-value format headers (e.g., 'http.request.header.date', 'http.request.header.content-type')\n // We'll replace them with the proper key-value format\n for (const key in extractedAttributes) {\n if (\n (key.startsWith(\"http.request.header.\") &&\n key !== \"http.request.header\") ||\n (key.startsWith(\"http.response.header.\") &&\n key !== \"http.response.header\")\n ) {\n // Remove both numeric index format and direct key-value format\n delete extractedAttributes[key];\n }\n }\n\n // Add filtered headers in proper key-value format\n if (Object.keys(requestHeaders).length > 0) {\n extractedAttributes[\"http.request.header\"] = requestHeaders;\n }\n\n if (Object.keys(responseHeaders).length > 0) {\n extractedAttributes[\"http.response.header\"] = responseHeaders;\n }\n\n // Remove body attributes if capture is disabled\n if (!shouldCaptureReqBody) {\n delete extractedAttributes[\"http.request.body\"];\n }\n\n if (!shouldCaptureRespBody) {\n delete extractedAttributes[\"http.response.body\"];\n }\n\n // Build span payload\n const spanContext = span.spanContext();\n // parentSpanId may not be available in all versions of ReadableSpan\n const parentSpanId =\n \"parentSpanId\" in span\n ? (span as ReadableSpan & { parentSpanId?: string }).parentSpanId\n : undefined;\n return {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n parentSpanId,\n name: span.name,\n kind: span.kind.toString(),\n startTime: new Date(\n span.startTime[0] * 1000 + span.startTime[1] / 1000000\n ).toISOString(),\n endTime: new Date(\n span.endTime[0] * 1000 + span.endTime[1] / 1000000\n ).toISOString(),\n duration:\n (span.endTime[0] - span.startTime[0]) * 1000 +\n (span.endTime[1] - span.startTime[1]) / 1000000,\n attributes: extractedAttributes,\n status: {\n code: span.status.code.toString(),\n message: span.status.message,\n },\n };\n}\n","/**\n * OpenTelemetry context keys for PingOps\n */\n\nimport { createContextKey } from \"@opentelemetry/api\";\n\n/**\n * Context key for trace ID attribute.\n * Used to propagate trace identifier to all spans in the context.\n */\nexport const PINGOPS_TRACE_ID = createContextKey(\"pingops-trace-id\");\n\n/**\n * Context key for user ID attribute.\n * Used to propagate user identifier to all spans in the context.\n */\nexport const PINGOPS_USER_ID = createContextKey(\"pingops-user-id\");\n\n/**\n * Context key for session ID attribute.\n * Used to propagate session identifier to all spans in the context.\n */\nexport const PINGOPS_SESSION_ID = createContextKey(\"pingops-session-id\");\n\n/**\n * Context key for tags attribute.\n * Used to propagate tags array to all spans in the context.\n */\nexport const PINGOPS_TAGS = createContextKey(\"pingops-tags\");\n\n/**\n * Context key for metadata attribute.\n * Used to propagate metadata object to all spans in the context.\n */\nexport const PINGOPS_METADATA = createContextKey(\"pingops-metadata\");\n\n/**\n * Context key for capturing request body.\n * When set, controls whether request bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_REQUEST_BODY = createContextKey(\n \"pingops-capture-request-body\"\n);\n\n/**\n * Context key for capturing response body.\n * When set, controls whether response bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_RESPONSE_BODY = createContextKey(\n \"pingops-capture-response-body\"\n);\n","/**\n * Extracts propagated attributes from OpenTelemetry context\n */\n\nimport type { Context } from \"@opentelemetry/api\";\nimport {\n PINGOPS_TRACE_ID,\n PINGOPS_USER_ID,\n PINGOPS_SESSION_ID,\n PINGOPS_TAGS,\n PINGOPS_METADATA,\n} from \"../context-keys\";\n\n/**\n * Extracts propagated attributes from the given context and returns them\n * as span attributes that can be set on a span.\n *\n * @param parentContext - The OpenTelemetry context to extract attributes from\n * @returns Record of attribute key-value pairs to set on spans\n */\nexport function getPropagatedAttributesFromContext(\n parentContext: Context\n): Record<string, string | string[]> {\n const attributes: Record<string, string | string[]> = {};\n\n // Extract traceId\n const traceId = parentContext.getValue(PINGOPS_TRACE_ID);\n if (traceId !== undefined && typeof traceId === \"string\") {\n attributes[\"pingops.trace_id\"] = traceId;\n }\n\n // Extract userId\n const userId = parentContext.getValue(PINGOPS_USER_ID);\n if (userId !== undefined && typeof userId === \"string\") {\n attributes[\"pingops.user_id\"] = userId;\n }\n\n // Extract sessionId\n const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);\n if (sessionId !== undefined && typeof sessionId === \"string\") {\n attributes[\"pingops.session_id\"] = sessionId;\n }\n\n // Extract tags\n const tags = parentContext.getValue(PINGOPS_TAGS);\n if (tags !== undefined && Array.isArray(tags)) {\n attributes[\"pingops.tags\"] = tags;\n }\n\n // Extract metadata\n const metadata = parentContext.getValue(PINGOPS_METADATA);\n if (\n metadata !== undefined &&\n typeof metadata === \"object\" &&\n metadata !== null &&\n !Array.isArray(metadata)\n ) {\n // Flatten metadata object into span attributes with prefix\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n attributes[`pingops.metadata.${key}`] = value;\n }\n }\n }\n\n return attributes;\n}\n","/**\n * Deterministic and random trace ID generation for PingOps\n */\n\n/**\n * Converts a Uint8Array to a lowercase hex string.\n */\nexport function uint8ArrayToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Creates a trace ID (32 hex chars).\n * - If `seed` is provided: deterministic via SHA-256 of the seed (first 32 hex chars).\n * - Otherwise: random 16 bytes as 32 hex chars.\n */\nexport async function createTraceId(seed?: string): Promise<string> {\n if (seed) {\n const data = new TextEncoder().encode(seed);\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const hashArray = new Uint8Array(hashBuffer);\n return uint8ArrayToHex(hashArray).slice(0, 32);\n }\n\n const randomValues = crypto.getRandomValues(new Uint8Array(16));\n return uint8ArrayToHex(randomValues);\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -54,7 +54,10 @@ interface PingopsTraceAttributes {
|
|
|
54
54
|
* Checks if a span is eligible for capture based on span kind and attributes.
|
|
55
55
|
* A span is eligible if:
|
|
56
56
|
* 1. span.kind === SpanKind.CLIENT
|
|
57
|
-
* 2. AND has HTTP attributes
|
|
57
|
+
* 2. AND has HTTP attributes
|
|
58
|
+
* - method: http.method or http.request.method
|
|
59
|
+
* - url: http.url or url.full
|
|
60
|
+
* - host: server.address
|
|
58
61
|
* OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)
|
|
59
62
|
*/
|
|
60
63
|
declare function isSpanEligible(span: ReadableSpan): boolean;
|
|
@@ -204,6 +207,28 @@ declare function bufferToBodyString(buffer: Buffer | null | undefined): string |
|
|
|
204
207
|
*/
|
|
205
208
|
declare function extractSpanPayload(span: ReadableSpan, domainAllowList?: DomainRule[], globalHeadersAllowList?: string[], globalHeadersDenyList?: string[], globalCaptureRequestBody?: boolean, globalCaptureResponseBody?: boolean, headerRedaction?: HeaderRedactionConfig): SpanPayload | null;
|
|
206
209
|
//#endregion
|
|
210
|
+
//#region src/utils/http-attributes.d.ts
|
|
211
|
+
/**
|
|
212
|
+
* Helpers for reading HTTP-related span attributes across legacy and modern
|
|
213
|
+
* OpenTelemetry semantic conventions.
|
|
214
|
+
*/
|
|
215
|
+
type SpanAttributes = Record<string, unknown>;
|
|
216
|
+
/**
|
|
217
|
+
* Returns true when either legacy or modern HTTP method attribute is present.
|
|
218
|
+
*/
|
|
219
|
+
declare function hasHttpMethodAttribute(attributes: SpanAttributes): boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Returns true when either legacy or modern HTTP URL attribute is present.
|
|
222
|
+
*/
|
|
223
|
+
declare function hasHttpUrlAttribute(attributes: SpanAttributes): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Extracts URL from known HTTP attributes with support for legacy + modern keys.
|
|
226
|
+
*
|
|
227
|
+
* If no explicit URL exists but server.address is available, falls back to a
|
|
228
|
+
* synthetic HTTPS URL for downstream domain filtering.
|
|
229
|
+
*/
|
|
230
|
+
declare function getHttpUrlFromAttributes(attributes: SpanAttributes): string | undefined;
|
|
231
|
+
//#endregion
|
|
207
232
|
//#region src/utils/context-extractor.d.ts
|
|
208
233
|
/**
|
|
209
234
|
* Extracts propagated attributes from the given context and returns them
|
|
@@ -291,5 +316,5 @@ declare function uint8ArrayToHex(bytes: Uint8Array): string;
|
|
|
291
316
|
*/
|
|
292
317
|
declare function createTraceId(seed?: string): Promise<string>;
|
|
293
318
|
//#endregion
|
|
294
|
-
export { DEFAULT_REDACTION_CONFIG, DEFAULT_SENSITIVE_HEADER_PATTERNS, DomainRule, HTTP_RESPONSE_CONTENT_ENCODING, HeaderRedactionConfig, HeaderRedactionStrategy, LogLevel, Logger, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, PINGOPS_METADATA, PINGOPS_SESSION_ID, PINGOPS_TAGS, PINGOPS_TRACE_ID, PINGOPS_USER_ID, PingopsTraceAttributes, SpanPayload, bufferToBodyString, createLogger, createTraceId, extractHeadersFromAttributes, extractSpanPayload, filterHeaders, getPropagatedAttributesFromContext, isCompressedContentEncoding, isSensitiveHeader, isSpanEligible, normalizeHeaders, redactHeaderValue, shouldCaptureSpan, uint8ArrayToHex };
|
|
319
|
+
export { DEFAULT_REDACTION_CONFIG, DEFAULT_SENSITIVE_HEADER_PATTERNS, DomainRule, HTTP_RESPONSE_CONTENT_ENCODING, HeaderRedactionConfig, HeaderRedactionStrategy, LogLevel, Logger, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, PINGOPS_METADATA, PINGOPS_SESSION_ID, PINGOPS_TAGS, PINGOPS_TRACE_ID, PINGOPS_USER_ID, PingopsTraceAttributes, SpanPayload, bufferToBodyString, createLogger, createTraceId, extractHeadersFromAttributes, extractSpanPayload, filterHeaders, getHttpUrlFromAttributes, getPropagatedAttributesFromContext, hasHttpMethodAttribute, hasHttpUrlAttribute, isCompressedContentEncoding, isSensitiveHeader, isSpanEligible, normalizeHeaders, redactHeaderValue, shouldCaptureSpan, uint8ArrayToHex };
|
|
295
320
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/utils/context-extractor.ts","../src/logger.ts","../src/context-keys.ts","../src/trace-id.ts"],"sourcesContent":[],"mappings":";;;;;;;UAIiB,UAAA;EAAA,MAAA,EAAA,MAAU;EASV,KAAA,CAAA,EAAA,MAAW,EAAA;EAmBX,gBAAA,CAAA,EAAA,MAAsB,EAAA;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/utils/http-attributes.ts","../src/utils/context-extractor.ts","../src/logger.ts","../src/context-keys.ts","../src/trace-id.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -54,7 +54,10 @@ interface PingopsTraceAttributes {
|
|
|
54
54
|
* Checks if a span is eligible for capture based on span kind and attributes.
|
|
55
55
|
* A span is eligible if:
|
|
56
56
|
* 1. span.kind === SpanKind.CLIENT
|
|
57
|
-
* 2. AND has HTTP attributes
|
|
57
|
+
* 2. AND has HTTP attributes
|
|
58
|
+
* - method: http.method or http.request.method
|
|
59
|
+
* - url: http.url or url.full
|
|
60
|
+
* - host: server.address
|
|
58
61
|
* OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)
|
|
59
62
|
*/
|
|
60
63
|
declare function isSpanEligible(span: ReadableSpan): boolean;
|
|
@@ -204,6 +207,28 @@ declare function bufferToBodyString(buffer: Buffer | null | undefined): string |
|
|
|
204
207
|
*/
|
|
205
208
|
declare function extractSpanPayload(span: ReadableSpan, domainAllowList?: DomainRule[], globalHeadersAllowList?: string[], globalHeadersDenyList?: string[], globalCaptureRequestBody?: boolean, globalCaptureResponseBody?: boolean, headerRedaction?: HeaderRedactionConfig): SpanPayload | null;
|
|
206
209
|
//#endregion
|
|
210
|
+
//#region src/utils/http-attributes.d.ts
|
|
211
|
+
/**
|
|
212
|
+
* Helpers for reading HTTP-related span attributes across legacy and modern
|
|
213
|
+
* OpenTelemetry semantic conventions.
|
|
214
|
+
*/
|
|
215
|
+
type SpanAttributes = Record<string, unknown>;
|
|
216
|
+
/**
|
|
217
|
+
* Returns true when either legacy or modern HTTP method attribute is present.
|
|
218
|
+
*/
|
|
219
|
+
declare function hasHttpMethodAttribute(attributes: SpanAttributes): boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Returns true when either legacy or modern HTTP URL attribute is present.
|
|
222
|
+
*/
|
|
223
|
+
declare function hasHttpUrlAttribute(attributes: SpanAttributes): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Extracts URL from known HTTP attributes with support for legacy + modern keys.
|
|
226
|
+
*
|
|
227
|
+
* If no explicit URL exists but server.address is available, falls back to a
|
|
228
|
+
* synthetic HTTPS URL for downstream domain filtering.
|
|
229
|
+
*/
|
|
230
|
+
declare function getHttpUrlFromAttributes(attributes: SpanAttributes): string | undefined;
|
|
231
|
+
//#endregion
|
|
207
232
|
//#region src/utils/context-extractor.d.ts
|
|
208
233
|
/**
|
|
209
234
|
* Extracts propagated attributes from the given context and returns them
|
|
@@ -291,5 +316,5 @@ declare function uint8ArrayToHex(bytes: Uint8Array): string;
|
|
|
291
316
|
*/
|
|
292
317
|
declare function createTraceId(seed?: string): Promise<string>;
|
|
293
318
|
//#endregion
|
|
294
|
-
export { DEFAULT_REDACTION_CONFIG, DEFAULT_SENSITIVE_HEADER_PATTERNS, DomainRule, HTTP_RESPONSE_CONTENT_ENCODING, HeaderRedactionConfig, HeaderRedactionStrategy, LogLevel, Logger, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, PINGOPS_METADATA, PINGOPS_SESSION_ID, PINGOPS_TAGS, PINGOPS_TRACE_ID, PINGOPS_USER_ID, PingopsTraceAttributes, SpanPayload, bufferToBodyString, createLogger, createTraceId, extractHeadersFromAttributes, extractSpanPayload, filterHeaders, getPropagatedAttributesFromContext, isCompressedContentEncoding, isSensitiveHeader, isSpanEligible, normalizeHeaders, redactHeaderValue, shouldCaptureSpan, uint8ArrayToHex };
|
|
319
|
+
export { DEFAULT_REDACTION_CONFIG, DEFAULT_SENSITIVE_HEADER_PATTERNS, DomainRule, HTTP_RESPONSE_CONTENT_ENCODING, HeaderRedactionConfig, HeaderRedactionStrategy, LogLevel, Logger, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, PINGOPS_METADATA, PINGOPS_SESSION_ID, PINGOPS_TAGS, PINGOPS_TRACE_ID, PINGOPS_USER_ID, PingopsTraceAttributes, SpanPayload, bufferToBodyString, createLogger, createTraceId, extractHeadersFromAttributes, extractSpanPayload, filterHeaders, getHttpUrlFromAttributes, getPropagatedAttributesFromContext, hasHttpMethodAttribute, hasHttpUrlAttribute, isCompressedContentEncoding, isSensitiveHeader, isSpanEligible, normalizeHeaders, redactHeaderValue, shouldCaptureSpan, uint8ArrayToHex };
|
|
295
320
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/utils/context-extractor.ts","../src/logger.ts","../src/context-keys.ts","../src/trace-id.ts"],"sourcesContent":[],"mappings":";;;;;;;UAIiB,UAAA;EAAA,MAAA,EAAA,MAAU;EASV,KAAA,CAAA,EAAA,MAAW,EAAA;EAmBX,gBAAA,CAAA,EAAA,MAAsB,EAAA;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/utils/http-attributes.ts","../src/utils/context-extractor.ts","../src/logger.ts","../src/context-keys.ts","../src/trace-id.ts"],"sourcesContent":[],"mappings":";;;;;;;UAIiB,UAAA;EAAA,MAAA,EAAA,MAAU;EASV,KAAA,CAAA,EAAA,MAAW,EAAA;EAmBX,gBAAA,CAAA,EAAA,MAAsB,EAAA;;;;ACRvC;UDXiB,WAAA;;;EEuDD,YAAA,CAAA,EAAA,MAAiB;;;;EC5DpB,OAAA,EAAA,MAAA;EAqED,QAAA,EAAA,MAAA;EAsBK,UAAA,EH7EH,MG6EG,CAAA,MAAqB,EAAA,OAAA,CAAA;EAmCzB,MAAA,EAAA;IAgBG,IAAA,EAAA,MAAA;IAuDA,OAAA,CAAA,EAAA,MAAiB;;;;AClIjC;;AAIoB,UJ/CH,sBAAA,CI+CG;EACjB,OAAA,CAAA,EAAA,MAAA;EAAM,MAAA,CAAA,EAAA,MAAA;EAyGO,SAAA,CAAA,EAAA,MAAA;EAyHA,IAAA,CAAA,EAAA,MAAA,EAAA;aJ7QH;;;AK9Bb;AA0CA;EAWgB,kBAAA,CAAA,EAAkB,OAAA;;;;ACoBlC;EACQ,mBAAA,CAAA,EAAA,OAAA;;;;AN7ER;AASA;AAmBA;;;;ACRA;;;;AC4CgB,iBD5CA,cAAA,CC8CI,IAAA,ED9CiB,YC+ClB,CAAU,EAAA,OAAA;;;AFnE7B;AASA;AAmBA;iBEoCgB,iBAAA,gCAEI,+BACD;;;;;;;AFnEnB;AASA;AAmBA;cGxBa;;;AFgBb;aEqDY,uBAAA;;;ADTZ;;;;AC5DA;EAqEY,OAAA,GAAA,SAAA;EAsBK;AAmCjB;AAgBA;EAuDgB,WAAA,GAAA,aAAiB;;;;EClIjB,MAAA,GAAA,QAAa;;;;;AA8Gb,UDtFC,qBAAA,CCsF2B;EAyH5B;;;;EC3SH,iBAAA,CAAA,EAAA,SAAA,MAA8B,EAAA;EA0C3B;AAWhB;;;aFkDa;EG9BG;;;;EAQb,eAAA,CAAA,EAAA,MAAA;EAAW;;;;ECnFT,YAAA,CAAA,EAAA,MAAc;EAKH;AAUhB;AAYA;;;;ACZA;;;cLkHa,0BAA0B,SAAS;AM/HhD;AAEA;AAaA;;;;ACZA;AAMA;AAMa,iBPgIG,iBAAA,COhIwD,UAAA,EAAA,MAAA,EAAA,QAAA,CAAA,EAAA,SAAA,MAAA,EAAA,CAAA,EAAA,OAAA;AAMxE;AAMA;AAMA;AAQa,iBP6JG,iBAAA,CO3Jf,KAAA,EAAA,MAAA,GAAA,MAAA,EAAA,GAAA,SAAA,EAAA,MAAA,EP6JS,QO7JT,CP6JkB,qBO7JlB,CAAA,CAAA,EAAA,MAAA,GAAA,MAAA,EAAA,GAAA,SAAA;;;AV9CD;AASA;AAmBA;;;;ACRA;;;;AC4CA;;;iBEOgB,aAAA,UACL,0HAGS,wBACjB;ADxEH;AAqEA;AAsBA;AAmCA;AAgBA;AAuDA;;;;AClIA;;;;;AA8GgB,iBAAA,4BAAA,CACF,UAEL,EAFK,MAEL,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,YAAA,EAAA,qBAAA,GAAA,sBAAA,CAAA,EAAN,MAAM,CAAA,MAAA,EAAA,MAAA,GAAA,MAAA,EAAA,GAAA,SAAA,CAAA,GAAA,IAAA;AAsHT;;;iBAAgB,gBAAA,oBAEb;;;;;;;AJhTH;AASA;AAmBiB,cKzBJ,8BAAA,GL8BM,gCAAA;;;;ACbnB;;iBIyBgB,2BAAA;;AHmBhB;;;iBGRgB,kBAAA,SACN;;;AL7BV;;;iBMgDgB,kBAAA,OACR,gCACY,8KAKA,wBACjB;;;;;;;ANpFH,KOCK,cAAA,GAAiB,MPDK,CAAA,MAAA,EAAA,OAAA,CAAA;AAS3B;AAmBA;;iBOtBgB,sBAAA,aAAmC;;ANcnD;;iBMJgB,mBAAA,aAAgC;;ALgDhD;;;;AC5DA;AAqEY,iBI7CI,wBAAA,CJ6CmB,UAAA,EI5CrB,cJ4CqB,CAAA,EAAA,MAAA,GAAA,SAAA;;;AHzEnC;AASA;AAmBA;;;;ACRA;iBOJgB,kCAAA,gBACC,UACd;;;;;;;ARlBH;AASA;AAmBiB,KSzBL,QAAA,GTyBK,OAAsB,GAAA,MAAA,GAK1B,MAAM,GAAA,OAAA;US5BF,MAAA;;;EReD,IAAA,CAAA,OAAA,EAAA,MAAc,EAAA,GAAA,IAAO,EAAA,OAAA,EAAA,CAAY,EAAA,IAAA;;;;AC4CjD;;;;AC5DA;AAqEY,iBMvDI,YAAA,CNuDmB,MAAA,EAAA,MAAA,CAAA,EMvDW,MNuDX;;;;;;;AHzEnC;AASA;AAmBA;cUtBa;;;ATcb;;cSRa;;ARoDb;;;cQ9Ca;APdb;AAqEA;AAsBA;AAmCA;AAgBgB,cO1HH,YP0HoB,EAAA,MAAA;AAuDjC;;;;AClIgB,cMzCH,gBNyCgB,EAAA,MAAA;;;;;AA8Gb,cMjJH,4BNkJC,EAAA,MAEX;AAsHH;;;;AC3Sa,cKyCA,6BLzC8B,EAAA,MAAA;;;;;;;ALH3C;AASA;AAmBiB,iBWzBD,eAAA,CX8BH,KAAA,EW9B0B,UX8BpB,CAAA,EAAA,MAAA;;;;ACbnB;;iBUNsB,aAAA,iBAA8B"}
|
package/dist/index.mjs
CHANGED
|
@@ -28,6 +28,35 @@ function createLogger(prefix) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/utils/http-attributes.ts
|
|
33
|
+
/**
|
|
34
|
+
* Returns true when either legacy or modern HTTP method attribute is present.
|
|
35
|
+
*/
|
|
36
|
+
function hasHttpMethodAttribute(attributes) {
|
|
37
|
+
return attributes["http.method"] !== void 0 || attributes["http.request.method"] !== void 0;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns true when either legacy or modern HTTP URL attribute is present.
|
|
41
|
+
*/
|
|
42
|
+
function hasHttpUrlAttribute(attributes) {
|
|
43
|
+
return attributes["http.url"] !== void 0 || attributes["url.full"] !== void 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Extracts URL from known HTTP attributes with support for legacy + modern keys.
|
|
47
|
+
*
|
|
48
|
+
* If no explicit URL exists but server.address is available, falls back to a
|
|
49
|
+
* synthetic HTTPS URL for downstream domain filtering.
|
|
50
|
+
*/
|
|
51
|
+
function getHttpUrlFromAttributes(attributes) {
|
|
52
|
+
const legacyUrl = attributes["http.url"];
|
|
53
|
+
if (typeof legacyUrl === "string" && legacyUrl.length > 0) return legacyUrl;
|
|
54
|
+
const modernUrl = attributes["url.full"];
|
|
55
|
+
if (typeof modernUrl === "string" && modernUrl.length > 0) return modernUrl;
|
|
56
|
+
const serverAddress = attributes["server.address"];
|
|
57
|
+
if (typeof serverAddress === "string" && serverAddress.length > 0) return `https://${serverAddress}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
//#endregion
|
|
32
61
|
//#region src/filtering/span-filter.ts
|
|
33
62
|
/**
|
|
@@ -38,7 +67,10 @@ const log$2 = createLogger("[PingOps SpanFilter]");
|
|
|
38
67
|
* Checks if a span is eligible for capture based on span kind and attributes.
|
|
39
68
|
* A span is eligible if:
|
|
40
69
|
* 1. span.kind === SpanKind.CLIENT
|
|
41
|
-
* 2. AND has HTTP attributes
|
|
70
|
+
* 2. AND has HTTP attributes
|
|
71
|
+
* - method: http.method or http.request.method
|
|
72
|
+
* - url: http.url or url.full
|
|
73
|
+
* - host: server.address
|
|
42
74
|
* OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)
|
|
43
75
|
*/
|
|
44
76
|
function isSpanEligible(span) {
|
|
@@ -56,8 +88,8 @@ function isSpanEligible(span) {
|
|
|
56
88
|
return false;
|
|
57
89
|
}
|
|
58
90
|
const attributes = span.attributes;
|
|
59
|
-
const hasHttpMethod = attributes
|
|
60
|
-
const hasHttpUrl = attributes
|
|
91
|
+
const hasHttpMethod = hasHttpMethodAttribute(attributes);
|
|
92
|
+
const hasHttpUrl = hasHttpUrlAttribute(attributes);
|
|
61
93
|
const hasServerAddress = attributes["server.address"] !== void 0;
|
|
62
94
|
const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;
|
|
63
95
|
log$2.debug("Span eligibility check result", {
|
|
@@ -66,6 +98,10 @@ function isSpanEligible(span) {
|
|
|
66
98
|
httpAttributes: {
|
|
67
99
|
hasMethod: hasHttpMethod,
|
|
68
100
|
hasUrl: hasHttpUrl,
|
|
101
|
+
hasLegacyMethod: attributes["http.method"] !== void 0,
|
|
102
|
+
hasModernMethod: attributes["http.request.method"] !== void 0,
|
|
103
|
+
hasLegacyUrl: attributes["http.url"] !== void 0,
|
|
104
|
+
hasModernUrl: attributes["url.full"] !== void 0,
|
|
69
105
|
hasServerAddress
|
|
70
106
|
}
|
|
71
107
|
});
|
|
@@ -367,6 +403,10 @@ function redactSingleValue(value, config) {
|
|
|
367
403
|
* Header filtering logic - applies allow/deny list rules and redaction
|
|
368
404
|
*/
|
|
369
405
|
const log = createLogger("[PingOps HeaderFilter]");
|
|
406
|
+
function toHeaderString(value) {
|
|
407
|
+
if (typeof value === "string") return value;
|
|
408
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
409
|
+
}
|
|
370
410
|
/**
|
|
371
411
|
* Normalizes header name to lowercase for case-insensitive matching
|
|
372
412
|
*/
|
|
@@ -528,7 +568,8 @@ function extractHeadersFromAttributes(attributes, headerPrefix) {
|
|
|
528
568
|
if (directKeyValueHeaders.length > 0) for (const { key, headerName } of directKeyValueHeaders) {
|
|
529
569
|
const headerValue = attributes[key];
|
|
530
570
|
if (headerValue !== void 0 && headerValue !== null) {
|
|
531
|
-
const stringValue =
|
|
571
|
+
const stringValue = toHeaderString(headerValue);
|
|
572
|
+
if (stringValue === void 0) continue;
|
|
532
573
|
const normalizedName = headerName.toLowerCase();
|
|
533
574
|
const existingKey = Object.keys(headerMap).find((k) => k.toLowerCase() === normalizedName);
|
|
534
575
|
if (existingKey) {
|
|
@@ -552,6 +593,13 @@ function normalizeHeaders(headers) {
|
|
|
552
593
|
const result = {};
|
|
553
594
|
if (!headers) return result;
|
|
554
595
|
try {
|
|
596
|
+
if (Array.isArray(headers)) {
|
|
597
|
+
for (let i = 0; i < headers.length; i += 2) if (i + 1 < headers.length) {
|
|
598
|
+
const key = String(headers[i]);
|
|
599
|
+
result[key] = headers[i + 1];
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
555
603
|
if (isHeadersLike(headers)) {
|
|
556
604
|
for (const [key, value] of headers.entries()) if (result[key]) {
|
|
557
605
|
const existing = result[key];
|
|
@@ -563,13 +611,6 @@ function normalizeHeaders(headers) {
|
|
|
563
611
|
for (const [key, value] of Object.entries(headers)) if (!/^\d+$/.test(key)) result[key] = value;
|
|
564
612
|
return result;
|
|
565
613
|
}
|
|
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
614
|
} catch {}
|
|
574
615
|
return result;
|
|
575
616
|
}
|
|
@@ -590,9 +631,16 @@ const COMPRESSED_ENCODINGS = new Set([
|
|
|
590
631
|
"x-gzip",
|
|
591
632
|
"x-deflate"
|
|
592
633
|
]);
|
|
634
|
+
function safeStringify(value) {
|
|
635
|
+
if (typeof value === "string") return value;
|
|
636
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
637
|
+
}
|
|
593
638
|
function normalizeHeaderValue(v) {
|
|
594
639
|
if (v == null) return void 0;
|
|
595
|
-
|
|
640
|
+
if (Array.isArray(v)) return v.map((item) => safeStringify(item)).filter((item) => item !== void 0).join(", ").trim() || void 0;
|
|
641
|
+
const s = safeStringify(v);
|
|
642
|
+
if (!s) return void 0;
|
|
643
|
+
return s.trim() || void 0;
|
|
596
644
|
}
|
|
597
645
|
/**
|
|
598
646
|
* Returns true if the content-encoding header indicates a compressed body
|
|
@@ -652,7 +700,7 @@ function shouldCaptureBody(domainRule, globalConfig, bodyType) {
|
|
|
652
700
|
*/
|
|
653
701
|
function extractSpanPayload(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction) {
|
|
654
702
|
const attributes = span.attributes;
|
|
655
|
-
const url = attributes
|
|
703
|
+
const url = getHttpUrlFromAttributes(attributes);
|
|
656
704
|
const domainRule = url ? getDomainRule(url, domainAllowList) : void 0;
|
|
657
705
|
const headersAllowList = domainRule?.headersAllowList ?? globalHeadersAllowList;
|
|
658
706
|
const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;
|
|
@@ -789,5 +837,5 @@ async function createTraceId(seed) {
|
|
|
789
837
|
}
|
|
790
838
|
|
|
791
839
|
//#endregion
|
|
792
|
-
export { DEFAULT_REDACTION_CONFIG, DEFAULT_SENSITIVE_HEADER_PATTERNS, HTTP_RESPONSE_CONTENT_ENCODING, HeaderRedactionStrategy, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, PINGOPS_METADATA, PINGOPS_SESSION_ID, PINGOPS_TAGS, PINGOPS_TRACE_ID, PINGOPS_USER_ID, bufferToBodyString, createLogger, createTraceId, extractHeadersFromAttributes, extractSpanPayload, filterHeaders, getPropagatedAttributesFromContext, isCompressedContentEncoding, isSensitiveHeader, isSpanEligible, normalizeHeaders, redactHeaderValue, shouldCaptureSpan, uint8ArrayToHex };
|
|
840
|
+
export { DEFAULT_REDACTION_CONFIG, DEFAULT_SENSITIVE_HEADER_PATTERNS, HTTP_RESPONSE_CONTENT_ENCODING, HeaderRedactionStrategy, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, PINGOPS_METADATA, PINGOPS_SESSION_ID, PINGOPS_TAGS, PINGOPS_TRACE_ID, PINGOPS_USER_ID, bufferToBodyString, createLogger, createTraceId, extractHeadersFromAttributes, extractSpanPayload, filterHeaders, getHttpUrlFromAttributes, getPropagatedAttributesFromContext, hasHttpMethodAttribute, hasHttpUrlAttribute, isCompressedContentEncoding, isSensitiveHeader, isSpanEligible, normalizeHeaders, redactHeaderValue, shouldCaptureSpan, uint8ArrayToHex };
|
|
793
841
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["log","log"],"sources":["../src/logger.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/context-keys.ts","../src/utils/context-extractor.ts","../src/trace-id.ts"],"sourcesContent":["/**\n * Global logger utility for PingOps Core\n *\n * Provides consistent logging across all core components with support for\n * different log levels and debug mode control via PINGOPS_DEBUG environment variable.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\n/**\n * Creates a logger instance with a specific prefix\n *\n * @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')\n * @returns Logger instance\n */\nexport function createLogger(prefix: string): Logger {\n const isDebugEnabled = process.env.PINGOPS_DEBUG === \"true\";\n\n const formatMessage = (level: LogLevel, message: string): string => {\n const timestamp = new Date().toISOString();\n return `[${timestamp}] ${prefix} [${level.toUpperCase()}] ${message}`;\n };\n\n return {\n debug(message: string, ...args: unknown[]): void {\n if (isDebugEnabled) {\n console.debug(formatMessage(\"debug\", message), ...args);\n }\n },\n info(message: string, ...args: unknown[]): void {\n console.log(formatMessage(\"info\", message), ...args);\n },\n warn(message: string, ...args: unknown[]): void {\n console.warn(formatMessage(\"warn\", message), ...args);\n },\n error(message: string, ...args: unknown[]): void {\n console.error(formatMessage(\"error\", message), ...args);\n },\n };\n}\n","/**\n * Span filtering logic - determines if a span is eligible for capture\n */\n\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps SpanFilter]\");\n\n/**\n * Checks if a span is eligible for capture based on span kind and attributes.\n * A span is eligible if:\n * 1. span.kind === SpanKind.CLIENT\n * 2. AND has HTTP attributes (http.method, http.url, or server.address)\n * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)\n */\nexport function isSpanEligible(span: ReadableSpan): boolean {\n log.debug(\"Checking span eligibility\", {\n spanName: span.name,\n spanKind: span.kind,\n spanId: span.spanContext().spanId,\n traceId: span.spanContext().traceId,\n });\n\n // Must be a CLIENT span (outgoing request)\n if (span.kind !== SpanKind.CLIENT) {\n log.debug(\"Span not eligible: not CLIENT kind\", {\n spanName: span.name,\n spanKind: span.kind,\n });\n return false;\n }\n\n const attributes = span.attributes;\n\n // Check for HTTP attributes\n const hasHttpMethod = attributes[\"http.method\"] !== undefined;\n const hasHttpUrl = attributes[\"http.url\"] !== undefined;\n const hasServerAddress = attributes[\"server.address\"] !== undefined;\n\n const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;\n\n log.debug(\"Span eligibility check result\", {\n spanName: span.name,\n isEligible,\n httpAttributes: {\n hasMethod: hasHttpMethod,\n hasUrl: hasHttpUrl,\n hasServerAddress,\n },\n });\n\n return isEligible;\n}\n","/**\n * Domain filtering logic - applies allow/deny list rules\n */\n\nimport type { DomainRule } from \"../types\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps DomainFilter]\");\n\n/**\n * Extracts domain from a URL\n */\nfunction extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n const domain = urlObj.hostname;\n log.debug(\"Extracted domain from URL\", { url, domain });\n return domain;\n } catch {\n // If URL parsing fails, try to extract domain from string\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n const domain = match ? match[1] : \"\";\n log.debug(\"Extracted domain from URL (fallback)\", { url, domain });\n return domain;\n }\n}\n\n/**\n * Checks if a domain matches a rule (exact or suffix match)\n */\nfunction domainMatches(domain: string, ruleDomain: string): boolean {\n // Exact match\n if (domain === ruleDomain) {\n log.debug(\"Domain exact match\", { domain, ruleDomain });\n return true;\n }\n\n // Suffix match (e.g., .github.com matches api.github.com)\n if (ruleDomain.startsWith(\".\")) {\n const matches =\n domain.endsWith(ruleDomain) || domain === ruleDomain.slice(1);\n log.debug(\"Domain suffix match check\", { domain, ruleDomain, matches });\n return matches;\n }\n\n log.debug(\"Domain does not match\", { domain, ruleDomain });\n return false;\n}\n\n/**\n * Checks if a path matches any of the allowed paths (prefix match)\n */\nfunction pathMatches(path: string, allowedPaths?: string[]): boolean {\n if (!allowedPaths || allowedPaths.length === 0) {\n log.debug(\"No path restrictions, all paths match\", { path });\n return true; // No path restrictions means all paths match\n }\n\n const matches = allowedPaths.some((allowedPath) =>\n path.startsWith(allowedPath)\n );\n log.debug(\"Path match check\", { path, allowedPaths, matches });\n return matches;\n}\n\n/**\n * Determines if a span should be captured based on domain rules\n */\nexport function shouldCaptureSpan(\n url: string,\n domainAllowList?: DomainRule[],\n domainDenyList?: DomainRule[]\n): boolean {\n log.debug(\"Checking domain filter rules\", {\n url,\n hasAllowList: !!domainAllowList && domainAllowList.length > 0,\n hasDenyList: !!domainDenyList && domainDenyList.length > 0,\n allowListCount: domainAllowList?.length || 0,\n denyListCount: domainDenyList?.length || 0,\n });\n\n const domain = extractDomain(url);\n\n // Extract path from URL\n let path = \"/\";\n try {\n const urlObj = new URL(url);\n path = urlObj.pathname;\n } catch {\n // If URL parsing fails, try to extract path from string\n const pathMatch = url.match(/^(?:https?:\\/\\/)?[^/]+(\\/.*)?$/);\n path = pathMatch && pathMatch[1] ? pathMatch[1] : \"/\";\n }\n\n log.debug(\"Extracted domain and path\", { url, domain, path });\n\n // Deny list is evaluated first - if domain is denied, don't capture\n if (domainDenyList) {\n for (const rule of domainDenyList) {\n if (domainMatches(domain, rule.domain)) {\n log.info(\"Domain denied by deny list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return false;\n }\n }\n log.debug(\"Domain passed deny list check\", { domain });\n }\n\n // If no allow list, capture all (except denied)\n if (!domainAllowList || domainAllowList.length === 0) {\n log.debug(\"No allow list configured, capturing span\", { domain, url });\n return true;\n }\n\n // Check if domain matches any allow list rule\n for (const rule of domainAllowList) {\n if (domainMatches(domain, rule.domain)) {\n // If paths are specified, check path match\n if (rule.paths && rule.paths.length > 0) {\n const pathMatch = pathMatches(path, rule.paths);\n if (pathMatch) {\n log.info(\"Domain and path allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n url,\n });\n return true;\n } else {\n log.debug(\"Domain allowed but path not matched\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n });\n }\n } else {\n log.info(\"Domain allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return true;\n }\n }\n }\n\n // Domain not in allow list\n log.info(\"Domain not in allow list, filtering out\", { domain, url });\n return false;\n}\n","/**\n * Sensitive header patterns and redaction configuration\n */\n\n/**\n * Default patterns for sensitive headers that should be redacted\n * These are matched case-insensitively\n */\nexport const DEFAULT_SENSITIVE_HEADER_PATTERNS = [\n // Authentication & Authorization\n \"authorization\",\n \"www-authenticate\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"x-auth-token\",\n \"x-api-key\",\n \"x-api-token\",\n \"x-access-token\",\n \"x-auth-user\",\n \"x-auth-password\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n\n // API Keys & Access Tokens\n \"api-key\",\n \"apikey\",\n \"api_key\",\n \"access-key\",\n \"accesskey\",\n \"access_key\",\n \"secret-key\",\n \"secretkey\",\n \"secret_key\",\n \"private-key\",\n \"privatekey\",\n \"private_key\",\n\n // Session & Cookie tokens\n \"cookie\",\n \"set-cookie\",\n \"session-id\",\n \"sessionid\",\n \"session_id\",\n \"session-token\",\n \"sessiontoken\",\n \"session_token\",\n\n // OAuth & OAuth2\n \"oauth-token\",\n \"oauth_token\",\n \"oauth2-token\",\n \"oauth2_token\",\n \"bearer\",\n\n // AWS & Cloud credentials\n \"x-amz-security-token\",\n \"x-amz-signature\",\n \"x-aws-access-key\",\n \"x-aws-secret-key\",\n \"x-aws-session-token\",\n\n // Other common sensitive headers\n \"x-password\",\n \"x-secret\",\n \"x-token\",\n \"x-jwt\",\n \"x-jwt-token\",\n \"x-refresh-token\",\n \"x-client-secret\",\n \"x-client-id\",\n \"x-user-token\",\n \"x-service-key\",\n] as const;\n\n/**\n * Redaction strategies for sensitive header values\n */\nexport enum HeaderRedactionStrategy {\n /**\n * Replace the entire value with a fixed redaction string\n */\n REPLACE = \"replace\",\n /**\n * Show only the first N characters, redact the rest\n */\n PARTIAL = \"partial\",\n /**\n * Show only the last N characters, redact the rest\n */\n PARTIAL_END = \"partial_end\",\n /**\n * Remove the header entirely (same as deny list)\n */\n REMOVE = \"remove\",\n}\n\n/**\n * Configuration for header redaction\n */\nexport interface HeaderRedactionConfig {\n /**\n * Patterns to match sensitive headers (case-insensitive)\n * Defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS if not provided\n */\n sensitivePatterns?: readonly string[];\n\n /**\n * Redaction strategy to use\n * @default HeaderRedactionStrategy.REPLACE\n */\n strategy?: HeaderRedactionStrategy;\n\n /**\n * Redaction string used when strategy is REPLACE\n * @default \"[REDACTED]\"\n */\n redactionString?: string;\n\n /**\n * Number of characters to show when strategy is PARTIAL or PARTIAL_END\n * @default 4\n */\n visibleChars?: number;\n\n /**\n * Whether to enable redaction\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Default redaction configuration\n */\nexport const DEFAULT_REDACTION_CONFIG: Required<HeaderRedactionConfig> = {\n sensitivePatterns: DEFAULT_SENSITIVE_HEADER_PATTERNS,\n strategy: HeaderRedactionStrategy.REPLACE,\n redactionString: \"[REDACTED]\",\n visibleChars: 4,\n enabled: true,\n};\n\n/**\n * Checks if a header name matches any sensitive pattern\n * Uses case-insensitive matching with exact match, prefix/suffix, and substring matching\n *\n * @param headerName - The header name to check\n * @param patterns - Array of patterns to match against (defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS)\n * @returns true if the header matches any sensitive pattern\n */\nexport function isSensitiveHeader(\n headerName: string,\n patterns: readonly string[] = DEFAULT_SENSITIVE_HEADER_PATTERNS\n): boolean {\n if (!headerName || typeof headerName !== \"string\") {\n return false;\n }\n\n if (!patterns || patterns.length === 0) {\n return false;\n }\n\n const normalizedName = headerName.toLowerCase().trim();\n\n // Early return for empty string\n if (normalizedName.length === 0) {\n return false;\n }\n\n return patterns.some((pattern) => {\n if (!pattern || typeof pattern !== \"string\") {\n return false;\n }\n\n const normalizedPattern = pattern.toLowerCase().trim();\n\n // Empty pattern doesn't match\n if (normalizedPattern.length === 0) {\n return false;\n }\n\n // Exact match (most common case, check first)\n if (normalizedName === normalizedPattern) {\n return true;\n }\n\n // Check if header name contains the pattern (e.g., \"x-api-key\" contains \"api-key\")\n // This handles cases where patterns are embedded in header names\n if (normalizedName.includes(normalizedPattern)) {\n return true;\n }\n\n // Check if pattern contains the header name (for shorter patterns matching longer headers)\n // This is less common but handles edge cases\n if (normalizedPattern.includes(normalizedName)) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Redacts a header value based on the configuration\n */\nexport function redactHeaderValue(\n value: string | string[] | undefined,\n config: Required<HeaderRedactionConfig>\n): string | string[] | undefined {\n if (value === undefined || value === null) {\n return value;\n }\n\n // Handle array of values\n if (Array.isArray(value)) {\n return value.map((v) => redactSingleValue(v, config));\n }\n\n return redactSingleValue(value, config);\n}\n\n/**\n * Redacts a single string value based on the configured strategy\n *\n * @param value - The value to redact\n * @param config - Redaction configuration\n * @returns Redacted value\n */\nfunction redactSingleValue(\n value: string,\n config: Required<HeaderRedactionConfig>\n): string {\n // Validate input\n if (!value || typeof value !== \"string\") {\n return value;\n }\n\n // Ensure visibleChars is a positive integer\n const visibleChars = Math.max(0, Math.floor(config.visibleChars || 0));\n const trimmedValue = value.trim();\n\n // Handle empty or very short values\n if (trimmedValue.length === 0) {\n return config.redactionString;\n }\n\n switch (config.strategy) {\n case HeaderRedactionStrategy.REPLACE:\n return config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL:\n // Show first N characters, then redaction string\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return trimmedValue.substring(0, visibleChars) + config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL_END:\n // Show last N characters, with redaction string prefix\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return (\n config.redactionString +\n trimmedValue.substring(trimmedValue.length - visibleChars)\n );\n\n case HeaderRedactionStrategy.REMOVE:\n // This should be handled at the filter level, not here\n // But if we reach here, return redaction string as fallback\n return config.redactionString;\n\n default:\n // Unknown strategy - default to full redaction for safety\n return config.redactionString;\n }\n}\n","/**\n * Header filtering logic - applies allow/deny list rules and redaction\n */\n\nimport { createLogger } from \"../logger\";\nimport type { HeaderRedactionConfig } from \"./sensitive-headers\";\nimport {\n DEFAULT_REDACTION_CONFIG,\n isSensitiveHeader,\n redactHeaderValue,\n HeaderRedactionStrategy,\n} from \"./sensitive-headers\";\n\nconst log = createLogger(\"[PingOps HeaderFilter]\");\n\n/**\n * Normalizes header name to lowercase for case-insensitive matching\n */\nfunction normalizeHeaderName(name: string): string {\n return name.toLowerCase();\n}\n\n/**\n * Merges redaction config with defaults\n */\nfunction mergeRedactionConfig(\n config?: HeaderRedactionConfig\n): Required<HeaderRedactionConfig> {\n // If config is undefined, use default config (enabled by default)\n if (!config) {\n return DEFAULT_REDACTION_CONFIG;\n }\n\n // If explicitly disabled, return disabled config\n if (config.enabled === false) {\n return { ...DEFAULT_REDACTION_CONFIG, enabled: false };\n }\n\n // Otherwise, merge with defaults (enabled defaults to true)\n return {\n sensitivePatterns:\n config.sensitivePatterns ?? DEFAULT_REDACTION_CONFIG.sensitivePatterns,\n strategy: config.strategy ?? DEFAULT_REDACTION_CONFIG.strategy,\n redactionString:\n config.redactionString ?? DEFAULT_REDACTION_CONFIG.redactionString,\n visibleChars: config.visibleChars ?? DEFAULT_REDACTION_CONFIG.visibleChars,\n enabled: config.enabled ?? DEFAULT_REDACTION_CONFIG.enabled,\n };\n}\n\n/**\n * Filters headers based on allow/deny lists and applies redaction to sensitive headers\n * - Deny list always wins (if header is in deny list, exclude it)\n * - Allow list filters included headers (if specified, only include these)\n * - Sensitive headers are redacted after filtering (if redaction is enabled)\n * - Case-insensitive matching\n *\n * @param headers - Headers to filter\n * @param headersAllowList - Optional allow list of header names to include\n * @param headersDenyList - Optional deny list of header names to exclude\n * @param redactionConfig - Optional configuration for header value redaction\n * @returns Filtered and redacted headers\n */\nexport function filterHeaders(\n headers: Record<string, string | string[] | undefined>,\n headersAllowList?: string[],\n headersDenyList?: string[],\n redactionConfig?: HeaderRedactionConfig\n): Record<string, string | string[] | undefined> {\n const originalCount = Object.keys(headers).length;\n const redaction = mergeRedactionConfig(redactionConfig);\n\n log.debug(\"Filtering headers\", {\n originalHeaderCount: originalCount,\n hasAllowList: !!headersAllowList && headersAllowList.length > 0,\n hasDenyList: !!headersDenyList && headersDenyList.length > 0,\n allowListCount: headersAllowList?.length || 0,\n denyListCount: headersDenyList?.length || 0,\n redactionEnabled: redaction.enabled,\n redactionStrategy: redaction.strategy,\n });\n\n const normalizedDenyList = headersDenyList?.map(normalizeHeaderName) ?? [];\n const normalizedAllowList = headersAllowList?.map(normalizeHeaderName) ?? [];\n\n const filtered: Record<string, string | string[] | undefined> = {};\n const deniedHeaders: string[] = [];\n const excludedHeaders: string[] = [];\n const redactedHeaders: string[] = [];\n\n for (const [name, value] of Object.entries(headers)) {\n const normalizedName = normalizeHeaderName(name);\n\n // Deny list always wins\n if (normalizedDenyList.includes(normalizedName)) {\n deniedHeaders.push(name);\n log.debug(\"Header denied by deny list\", { headerName: name });\n continue;\n }\n\n // If allow list exists, only include headers in the list\n if (normalizedAllowList.length > 0) {\n if (!normalizedAllowList.includes(normalizedName)) {\n excludedHeaders.push(name);\n log.debug(\"Header excluded (not in allow list)\", { headerName: name });\n continue;\n }\n }\n\n // Apply redaction if enabled and header is sensitive\n let finalValue = value;\n if (redaction.enabled) {\n try {\n // Check if header matches sensitive patterns\n if (isSensitiveHeader(name, redaction.sensitivePatterns)) {\n // Handle REMOVE strategy at filter level\n if (redaction.strategy === HeaderRedactionStrategy.REMOVE) {\n log.debug(\"Header removed by redaction strategy\", {\n headerName: name,\n });\n continue;\n }\n\n // Redact the value\n finalValue = redactHeaderValue(value, redaction);\n redactedHeaders.push(name);\n log.debug(\"Header value redacted\", {\n headerName: name,\n strategy: redaction.strategy,\n });\n }\n } catch (error) {\n // Log error but don't fail - use original value as fallback\n log.warn(\"Error redacting header value\", {\n headerName: name,\n error: error instanceof Error ? error.message : String(error),\n });\n finalValue = value;\n }\n }\n\n filtered[name] = finalValue;\n }\n\n const filteredCount = Object.keys(filtered).length;\n log.info(\"Header filtering complete\", {\n originalCount,\n filteredCount,\n deniedCount: deniedHeaders.length,\n excludedCount: excludedHeaders.length,\n redactedCount: redactedHeaders.length,\n deniedHeaders: deniedHeaders.length > 0 ? deniedHeaders : undefined,\n excludedHeaders: excludedHeaders.length > 0 ? excludedHeaders : undefined,\n redactedHeaders: redactedHeaders.length > 0 ? redactedHeaders : undefined,\n });\n\n return filtered;\n}\n\n/**\n * Extracts and normalizes headers from OpenTelemetry span attributes\n *\n * Handles two formats:\n * 1. Flat array format (e.g., 'http.request.header.0', 'http.request.header.1')\n * - 'http.request.header.0': 'Content-Type'\n * - 'http.request.header.1': 'application/json'\n * 2. Direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n * - 'http.request.header.date': 'Mon, 12 Jan 2026 20:22:38 GMT'\n * - 'http.request.header.content-type': 'application/json'\n *\n * This function converts them to:\n * - { 'Content-Type': 'application/json', 'date': 'Mon, 12 Jan 2026 20:22:38 GMT' }\n */\nexport function extractHeadersFromAttributes(\n attributes: Record<string, unknown>,\n headerPrefix: \"http.request.header\" | \"http.response.header\"\n): Record<string, string | string[] | undefined> | null {\n const headerMap: Record<string, string | string[] | undefined> = {};\n const headerKeys: number[] = [];\n const directKeyValueHeaders: Array<{ key: string; headerName: string }> = [];\n\n const prefixPattern = `${headerPrefix}.`;\n const numericPattern = new RegExp(\n `^${headerPrefix.replace(/\\./g, \"\\\\.\")}\\\\.(\\\\d+)$`\n );\n\n // Find all keys matching the pattern\n for (const key in attributes) {\n if (key.startsWith(prefixPattern) && key !== headerPrefix) {\n // Check for numeric index format (flat array)\n const numericMatch = key.match(numericPattern);\n if (numericMatch) {\n const index = parseInt(numericMatch[1], 10);\n headerKeys.push(index);\n } else {\n // Check for direct key-value format (e.g., 'http.request.header.date')\n const headerName = key.substring(prefixPattern.length);\n if (headerName.length > 0) {\n directKeyValueHeaders.push({ key, headerName });\n }\n }\n }\n }\n\n // Process numeric index format (flat array)\n if (headerKeys.length > 0) {\n // Sort indices to process in order\n headerKeys.sort((a, b) => a - b);\n\n // Convert flat array to key-value pairs\n // Even indices are header names, odd indices are header values\n for (let i = 0; i < headerKeys.length; i += 2) {\n const nameIndex = headerKeys[i];\n const valueIndex = headerKeys[i + 1];\n\n if (valueIndex !== undefined) {\n const nameKey = `${headerPrefix}.${nameIndex}`;\n const valueKey = `${headerPrefix}.${valueIndex}`;\n\n const headerName = attributes[nameKey] as string | undefined;\n const headerValue = attributes[valueKey] as string | undefined;\n\n if (headerName && headerValue !== undefined) {\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, headerValue]\n : [existing as string, headerValue];\n } else {\n // Use original case for the first occurrence\n headerMap[headerName] = headerValue;\n }\n }\n }\n }\n }\n\n // Process direct key-value format (e.g., 'http.request.header.date')\n if (directKeyValueHeaders.length > 0) {\n for (const { key, headerName } of directKeyValueHeaders) {\n const headerValue = attributes[key];\n\n if (headerValue !== undefined && headerValue !== null) {\n // Convert to string if needed\n const stringValue =\n typeof headerValue === \"string\" ? headerValue : String(headerValue);\n\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, stringValue]\n : [existing as string, stringValue];\n } else {\n // Use the header name as stored (may be lowercase from instrumentation)\n headerMap[headerName] = stringValue;\n }\n }\n }\n }\n\n return Object.keys(headerMap).length > 0 ? headerMap : null;\n}\n\n/**\n * Type guard to check if value is a Headers-like object\n */\nfunction isHeadersLike(\n headers: unknown\n): headers is { entries: () => IterableIterator<[string, string]> } {\n return (\n typeof headers === \"object\" &&\n headers !== null &&\n \"entries\" in headers &&\n typeof (headers as { entries?: unknown }).entries === \"function\"\n );\n}\n\n/**\n * Normalizes headers from various sources into a proper key-value object\n */\nexport function normalizeHeaders(\n headers: unknown\n): Record<string, string | string[] | undefined> {\n const result: Record<string, string | string[] | undefined> = {};\n\n if (!headers) {\n return result;\n }\n\n try {\n // Handle Headers object (from fetch/undici)\n if (isHeadersLike(headers)) {\n for (const [key, value] of headers.entries()) {\n // Headers can have multiple values for the same key\n if (result[key]) {\n // Convert to array if not already\n const existing = result[key];\n result[key] = Array.isArray(existing)\n ? [...existing, value]\n : [existing, value];\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n // Handle plain object\n if (typeof headers === \"object\" && !Array.isArray(headers)) {\n for (const [key, value] of Object.entries(headers)) {\n // Skip numeric keys (array-like objects)\n if (!/^\\d+$/.test(key)) {\n result[key] = value as string | string[] | undefined;\n }\n }\n return result;\n }\n\n // Handle array (shouldn't happen, but handle gracefully)\n if (Array.isArray(headers)) {\n // Try to reconstruct from array pairs\n for (let i = 0; i < headers.length; i += 2) {\n if (i + 1 < headers.length) {\n const key = String(headers[i]);\n const value = headers[i + 1] as string | string[] | undefined;\n result[key] = value;\n }\n }\n return result;\n }\n } catch {\n // Fail silently - return empty object\n }\n\n return result;\n}\n","/**\n * Minimal body handling: buffer to string for span attributes.\n * No decompression or truncation; for compressed responses the instrumentation\n * sends base64 + content-encoding so the backend can decompress.\n */\n\n/** Span attribute for response content-encoding when body is sent as base64. */\nexport const HTTP_RESPONSE_CONTENT_ENCODING = \"http.response.content_encoding\";\n\nconst COMPRESSED_ENCODINGS = new Set([\n \"gzip\",\n \"br\",\n \"deflate\",\n \"x-gzip\",\n \"x-deflate\",\n]);\n\nfunction normalizeHeaderValue(v: unknown): string | undefined {\n if (v == null) return undefined;\n const s = Array.isArray(v) ? v.map(String).join(\", \") : String(v);\n return s.trim() || undefined;\n}\n\n/**\n * Returns true if the content-encoding header indicates a compressed body\n * (gzip, br, deflate, x-gzip, x-deflate). Used to decide whether to send\n * body as base64 + content-encoding for backend decompression.\n */\nexport function isCompressedContentEncoding(headerValue: unknown): boolean {\n const raw = normalizeHeaderValue(headerValue);\n if (!raw) return false;\n const first = raw.split(\",\")[0].trim().toLowerCase();\n return COMPRESSED_ENCODINGS.has(first);\n}\n\n/**\n * Converts a buffer to a UTF-8 string for use as request/response body on spans.\n * Returns null for null, undefined, or empty buffer.\n */\nexport function bufferToBodyString(\n buffer: Buffer | null | undefined\n): string | null {\n if (buffer == null || buffer.length === 0) return null;\n return buffer.toString(\"utf8\");\n}\n","/**\n * Extracts structured data from spans for PingOps backend\n */\n\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport type { DomainRule, SpanPayload } from \"../types\";\nimport type { HeaderRedactionConfig } from \"../filtering/sensitive-headers\";\nimport {\n filterHeaders,\n extractHeadersFromAttributes,\n} from \"../filtering/header-filter\";\n\n/**\n * Extracts domain from URL\n */\nfunction extractDomainFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n return match ? match[1] : \"\";\n }\n}\n\n/**\n * Gets domain rule configuration for a given URL\n */\nfunction getDomainRule(\n url: string,\n domainAllowList?: DomainRule[]\n): DomainRule | undefined {\n if (!domainAllowList) {\n return undefined;\n }\n\n const domain = extractDomainFromUrl(url);\n for (const rule of domainAllowList) {\n if (\n domain === rule.domain ||\n domain.endsWith(`.${rule.domain}`) ||\n domain === rule.domain.slice(1)\n ) {\n return rule;\n }\n }\n return undefined;\n}\n\n/**\n * Determines if body should be captured based on priority:\n * domain rule > global config > default (false)\n */\nfunction shouldCaptureBody(\n domainRule: DomainRule | undefined,\n globalConfig: boolean | undefined,\n bodyType: \"request\" | \"response\"\n): boolean {\n // Check domain-specific rule first\n if (domainRule) {\n const domainValue =\n bodyType === \"request\"\n ? domainRule.captureRequestBody\n : domainRule.captureResponseBody;\n if (domainValue !== undefined) {\n return domainValue;\n }\n }\n // Fall back to global config\n if (globalConfig !== undefined) {\n return globalConfig;\n }\n // Default to false\n return false;\n}\n\n/**\n * Extracts structured payload from a span\n */\nexport function extractSpanPayload(\n span: ReadableSpan,\n domainAllowList?: DomainRule[],\n globalHeadersAllowList?: string[],\n globalHeadersDenyList?: string[],\n globalCaptureRequestBody?: boolean,\n globalCaptureResponseBody?: boolean,\n headerRedaction?: HeaderRedactionConfig\n): SpanPayload | null {\n const attributes = span.attributes;\n const url =\n (attributes[\"http.url\"] as string) || (attributes[\"url.full\"] as string);\n\n // Get domain-specific rule if available\n const domainRule = url ? getDomainRule(url, domainAllowList) : undefined;\n\n // Merge global and domain-specific header rules\n const headersAllowList =\n domainRule?.headersAllowList ?? globalHeadersAllowList;\n const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;\n\n // Determine if bodies should be captured\n const shouldCaptureReqBody = shouldCaptureBody(\n domainRule,\n globalCaptureRequestBody,\n \"request\"\n );\n const shouldCaptureRespBody = shouldCaptureBody(\n domainRule,\n globalCaptureResponseBody,\n \"response\"\n );\n\n // Extract HTTP headers if available\n let requestHeaders: Record<string, string | string[] | undefined> = {};\n let responseHeaders: Record<string, string | string[] | undefined> = {};\n\n // First, try to extract flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1')\n // or direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n const flatRequestHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.request.header\"\n );\n const flatResponseHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.response.header\"\n );\n\n // Try to get headers from attributes (format may vary by instrumentation)\n const httpRequestHeadersValue = attributes[\"http.request.header\"];\n const httpResponseHeadersValue = attributes[\"http.response.header\"];\n\n // Type guard: check if value is a record/object with string keys\n const isHeadersRecord = (\n value: unknown\n ): value is Record<string, string | string[] | undefined> => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value).every(\n (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((item) => typeof item === \"string\")) ||\n v === undefined\n )\n );\n };\n\n // Use flat array format if available, otherwise use direct attribute\n if (flatRequestHeaders) {\n requestHeaders = filterHeaders(\n flatRequestHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpRequestHeadersValue)) {\n requestHeaders = filterHeaders(\n httpRequestHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n if (flatResponseHeaders) {\n responseHeaders = filterHeaders(\n flatResponseHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpResponseHeadersValue)) {\n responseHeaders = filterHeaders(\n httpResponseHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n // Build attributes object\n const extractedAttributes: Record<string, unknown> = {\n ...attributes,\n };\n\n // Remove flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1', etc.)\n // and direct key-value format headers (e.g., 'http.request.header.date', 'http.request.header.content-type')\n // We'll replace them with the proper key-value format\n for (const key in extractedAttributes) {\n if (\n (key.startsWith(\"http.request.header.\") &&\n key !== \"http.request.header\") ||\n (key.startsWith(\"http.response.header.\") &&\n key !== \"http.response.header\")\n ) {\n // Remove both numeric index format and direct key-value format\n delete extractedAttributes[key];\n }\n }\n\n // Add filtered headers in proper key-value format\n if (Object.keys(requestHeaders).length > 0) {\n extractedAttributes[\"http.request.header\"] = requestHeaders;\n }\n\n if (Object.keys(responseHeaders).length > 0) {\n extractedAttributes[\"http.response.header\"] = responseHeaders;\n }\n\n // Remove body attributes if capture is disabled\n if (!shouldCaptureReqBody) {\n delete extractedAttributes[\"http.request.body\"];\n }\n\n if (!shouldCaptureRespBody) {\n delete extractedAttributes[\"http.response.body\"];\n }\n\n // Build span payload\n const spanContext = span.spanContext();\n // parentSpanId may not be available in all versions of ReadableSpan\n const parentSpanId =\n \"parentSpanId\" in span\n ? (span as ReadableSpan & { parentSpanId?: string }).parentSpanId\n : undefined;\n return {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n parentSpanId,\n name: span.name,\n kind: span.kind.toString(),\n startTime: new Date(\n span.startTime[0] * 1000 + span.startTime[1] / 1000000\n ).toISOString(),\n endTime: new Date(\n span.endTime[0] * 1000 + span.endTime[1] / 1000000\n ).toISOString(),\n duration:\n (span.endTime[0] - span.startTime[0]) * 1000 +\n (span.endTime[1] - span.startTime[1]) / 1000000,\n attributes: extractedAttributes,\n status: {\n code: span.status.code.toString(),\n message: span.status.message,\n },\n };\n}\n","/**\n * OpenTelemetry context keys for PingOps\n */\n\nimport { createContextKey } from \"@opentelemetry/api\";\n\n/**\n * Context key for trace ID attribute.\n * Used to propagate trace identifier to all spans in the context.\n */\nexport const PINGOPS_TRACE_ID = createContextKey(\"pingops-trace-id\");\n\n/**\n * Context key for user ID attribute.\n * Used to propagate user identifier to all spans in the context.\n */\nexport const PINGOPS_USER_ID = createContextKey(\"pingops-user-id\");\n\n/**\n * Context key for session ID attribute.\n * Used to propagate session identifier to all spans in the context.\n */\nexport const PINGOPS_SESSION_ID = createContextKey(\"pingops-session-id\");\n\n/**\n * Context key for tags attribute.\n * Used to propagate tags array to all spans in the context.\n */\nexport const PINGOPS_TAGS = createContextKey(\"pingops-tags\");\n\n/**\n * Context key for metadata attribute.\n * Used to propagate metadata object to all spans in the context.\n */\nexport const PINGOPS_METADATA = createContextKey(\"pingops-metadata\");\n\n/**\n * Context key for capturing request body.\n * When set, controls whether request bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_REQUEST_BODY = createContextKey(\n \"pingops-capture-request-body\"\n);\n\n/**\n * Context key for capturing response body.\n * When set, controls whether response bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_RESPONSE_BODY = createContextKey(\n \"pingops-capture-response-body\"\n);\n","/**\n * Extracts propagated attributes from OpenTelemetry context\n */\n\nimport type { Context } from \"@opentelemetry/api\";\nimport {\n PINGOPS_TRACE_ID,\n PINGOPS_USER_ID,\n PINGOPS_SESSION_ID,\n PINGOPS_TAGS,\n PINGOPS_METADATA,\n} from \"../context-keys\";\n\n/**\n * Extracts propagated attributes from the given context and returns them\n * as span attributes that can be set on a span.\n *\n * @param parentContext - The OpenTelemetry context to extract attributes from\n * @returns Record of attribute key-value pairs to set on spans\n */\nexport function getPropagatedAttributesFromContext(\n parentContext: Context\n): Record<string, string | string[]> {\n const attributes: Record<string, string | string[]> = {};\n\n // Extract traceId\n const traceId = parentContext.getValue(PINGOPS_TRACE_ID);\n if (traceId !== undefined && typeof traceId === \"string\") {\n attributes[\"pingops.trace_id\"] = traceId;\n }\n\n // Extract userId\n const userId = parentContext.getValue(PINGOPS_USER_ID);\n if (userId !== undefined && typeof userId === \"string\") {\n attributes[\"pingops.user_id\"] = userId;\n }\n\n // Extract sessionId\n const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);\n if (sessionId !== undefined && typeof sessionId === \"string\") {\n attributes[\"pingops.session_id\"] = sessionId;\n }\n\n // Extract tags\n const tags = parentContext.getValue(PINGOPS_TAGS);\n if (tags !== undefined && Array.isArray(tags)) {\n attributes[\"pingops.tags\"] = tags;\n }\n\n // Extract metadata\n const metadata = parentContext.getValue(PINGOPS_METADATA);\n if (\n metadata !== undefined &&\n typeof metadata === \"object\" &&\n metadata !== null &&\n !Array.isArray(metadata)\n ) {\n // Flatten metadata object into span attributes with prefix\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n attributes[`pingops.metadata.${key}`] = value;\n }\n }\n }\n\n return attributes;\n}\n","/**\n * Deterministic and random trace ID generation for PingOps\n */\n\n/**\n * Converts a Uint8Array to a lowercase hex string.\n */\nexport function uint8ArrayToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Creates a trace ID (32 hex chars).\n * - If `seed` is provided: deterministic via SHA-256 of the seed (first 32 hex chars).\n * - Otherwise: random 16 bytes as 32 hex chars.\n */\nexport async function createTraceId(seed?: string): Promise<string> {\n if (seed) {\n const data = new TextEncoder().encode(seed);\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const hashArray = new Uint8Array(hashBuffer);\n return uint8ArrayToHex(hashArray).slice(0, 32);\n }\n\n const randomValues = crypto.getRandomValues(new Uint8Array(16));\n return uint8ArrayToHex(randomValues);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["log","log"],"sources":["../src/logger.ts","../src/utils/http-attributes.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/context-keys.ts","../src/utils/context-extractor.ts","../src/trace-id.ts"],"sourcesContent":["/**\n * Global logger utility for PingOps Core\n *\n * Provides consistent logging across all core components with support for\n * different log levels and debug mode control via PINGOPS_DEBUG environment variable.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\n/**\n * Creates a logger instance with a specific prefix\n *\n * @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')\n * @returns Logger instance\n */\nexport function createLogger(prefix: string): Logger {\n const isDebugEnabled = process.env.PINGOPS_DEBUG === \"true\";\n\n const formatMessage = (level: LogLevel, message: string): string => {\n const timestamp = new Date().toISOString();\n return `[${timestamp}] ${prefix} [${level.toUpperCase()}] ${message}`;\n };\n\n return {\n debug(message: string, ...args: unknown[]): void {\n if (isDebugEnabled) {\n console.debug(formatMessage(\"debug\", message), ...args);\n }\n },\n info(message: string, ...args: unknown[]): void {\n console.log(formatMessage(\"info\", message), ...args);\n },\n warn(message: string, ...args: unknown[]): void {\n console.warn(formatMessage(\"warn\", message), ...args);\n },\n error(message: string, ...args: unknown[]): void {\n console.error(formatMessage(\"error\", message), ...args);\n },\n };\n}\n","/**\n * Helpers for reading HTTP-related span attributes across legacy and modern\n * OpenTelemetry semantic conventions.\n */\n\ntype SpanAttributes = Record<string, unknown>;\n\n/**\n * Returns true when either legacy or modern HTTP method attribute is present.\n */\nexport function hasHttpMethodAttribute(attributes: SpanAttributes): boolean {\n return (\n attributes[\"http.method\"] !== undefined ||\n attributes[\"http.request.method\"] !== undefined\n );\n}\n\n/**\n * Returns true when either legacy or modern HTTP URL attribute is present.\n */\nexport function hasHttpUrlAttribute(attributes: SpanAttributes): boolean {\n return (\n attributes[\"http.url\"] !== undefined || attributes[\"url.full\"] !== undefined\n );\n}\n\n/**\n * Extracts URL from known HTTP attributes with support for legacy + modern keys.\n *\n * If no explicit URL exists but server.address is available, falls back to a\n * synthetic HTTPS URL for downstream domain filtering.\n */\nexport function getHttpUrlFromAttributes(\n attributes: SpanAttributes\n): string | undefined {\n const legacyUrl = attributes[\"http.url\"];\n if (typeof legacyUrl === \"string\" && legacyUrl.length > 0) {\n return legacyUrl;\n }\n\n const modernUrl = attributes[\"url.full\"];\n if (typeof modernUrl === \"string\" && modernUrl.length > 0) {\n return modernUrl;\n }\n\n const serverAddress = attributes[\"server.address\"];\n if (typeof serverAddress === \"string\" && serverAddress.length > 0) {\n return `https://${serverAddress}`;\n }\n\n return undefined;\n}\n","/**\n * Span filtering logic - determines if a span is eligible for capture\n */\n\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { createLogger } from \"../logger\";\nimport {\n hasHttpMethodAttribute,\n hasHttpUrlAttribute,\n} from \"../utils/http-attributes\";\n\nconst log = createLogger(\"[PingOps SpanFilter]\");\n\n/**\n * Checks if a span is eligible for capture based on span kind and attributes.\n * A span is eligible if:\n * 1. span.kind === SpanKind.CLIENT\n * 2. AND has HTTP attributes\n * - method: http.method or http.request.method\n * - url: http.url or url.full\n * - host: server.address\n * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)\n */\nexport function isSpanEligible(span: ReadableSpan): boolean {\n log.debug(\"Checking span eligibility\", {\n spanName: span.name,\n spanKind: span.kind,\n spanId: span.spanContext().spanId,\n traceId: span.spanContext().traceId,\n });\n\n // Must be a CLIENT span (outgoing request)\n if (span.kind !== SpanKind.CLIENT) {\n log.debug(\"Span not eligible: not CLIENT kind\", {\n spanName: span.name,\n spanKind: span.kind,\n });\n return false;\n }\n\n const attributes = span.attributes;\n\n // Check for HTTP attributes\n const hasHttpMethod = hasHttpMethodAttribute(attributes);\n const hasHttpUrl = hasHttpUrlAttribute(attributes);\n const hasServerAddress = attributes[\"server.address\"] !== undefined;\n\n const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;\n\n log.debug(\"Span eligibility check result\", {\n spanName: span.name,\n isEligible,\n httpAttributes: {\n hasMethod: hasHttpMethod,\n hasUrl: hasHttpUrl,\n hasLegacyMethod: attributes[\"http.method\"] !== undefined,\n hasModernMethod: attributes[\"http.request.method\"] !== undefined,\n hasLegacyUrl: attributes[\"http.url\"] !== undefined,\n hasModernUrl: attributes[\"url.full\"] !== undefined,\n hasServerAddress,\n },\n });\n\n return isEligible;\n}\n","/**\n * Domain filtering logic - applies allow/deny list rules\n */\n\nimport type { DomainRule } from \"../types\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps DomainFilter]\");\n\n/**\n * Extracts domain from a URL\n */\nfunction extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n const domain = urlObj.hostname;\n log.debug(\"Extracted domain from URL\", { url, domain });\n return domain;\n } catch {\n // If URL parsing fails, try to extract domain from string\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n const domain = match ? match[1] : \"\";\n log.debug(\"Extracted domain from URL (fallback)\", { url, domain });\n return domain;\n }\n}\n\n/**\n * Checks if a domain matches a rule (exact or suffix match)\n */\nfunction domainMatches(domain: string, ruleDomain: string): boolean {\n // Exact match\n if (domain === ruleDomain) {\n log.debug(\"Domain exact match\", { domain, ruleDomain });\n return true;\n }\n\n // Suffix match (e.g., .github.com matches api.github.com)\n if (ruleDomain.startsWith(\".\")) {\n const matches =\n domain.endsWith(ruleDomain) || domain === ruleDomain.slice(1);\n log.debug(\"Domain suffix match check\", { domain, ruleDomain, matches });\n return matches;\n }\n\n log.debug(\"Domain does not match\", { domain, ruleDomain });\n return false;\n}\n\n/**\n * Checks if a path matches any of the allowed paths (prefix match)\n */\nfunction pathMatches(path: string, allowedPaths?: string[]): boolean {\n if (!allowedPaths || allowedPaths.length === 0) {\n log.debug(\"No path restrictions, all paths match\", { path });\n return true; // No path restrictions means all paths match\n }\n\n const matches = allowedPaths.some((allowedPath) =>\n path.startsWith(allowedPath)\n );\n log.debug(\"Path match check\", { path, allowedPaths, matches });\n return matches;\n}\n\n/**\n * Determines if a span should be captured based on domain rules\n */\nexport function shouldCaptureSpan(\n url: string,\n domainAllowList?: DomainRule[],\n domainDenyList?: DomainRule[]\n): boolean {\n log.debug(\"Checking domain filter rules\", {\n url,\n hasAllowList: !!domainAllowList && domainAllowList.length > 0,\n hasDenyList: !!domainDenyList && domainDenyList.length > 0,\n allowListCount: domainAllowList?.length || 0,\n denyListCount: domainDenyList?.length || 0,\n });\n\n const domain = extractDomain(url);\n\n // Extract path from URL\n let path = \"/\";\n try {\n const urlObj = new URL(url);\n path = urlObj.pathname;\n } catch {\n // If URL parsing fails, try to extract path from string\n const pathMatch = url.match(/^(?:https?:\\/\\/)?[^/]+(\\/.*)?$/);\n path = pathMatch && pathMatch[1] ? pathMatch[1] : \"/\";\n }\n\n log.debug(\"Extracted domain and path\", { url, domain, path });\n\n // Deny list is evaluated first - if domain is denied, don't capture\n if (domainDenyList) {\n for (const rule of domainDenyList) {\n if (domainMatches(domain, rule.domain)) {\n log.info(\"Domain denied by deny list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return false;\n }\n }\n log.debug(\"Domain passed deny list check\", { domain });\n }\n\n // If no allow list, capture all (except denied)\n if (!domainAllowList || domainAllowList.length === 0) {\n log.debug(\"No allow list configured, capturing span\", { domain, url });\n return true;\n }\n\n // Check if domain matches any allow list rule\n for (const rule of domainAllowList) {\n if (domainMatches(domain, rule.domain)) {\n // If paths are specified, check path match\n if (rule.paths && rule.paths.length > 0) {\n const pathMatch = pathMatches(path, rule.paths);\n if (pathMatch) {\n log.info(\"Domain and path allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n url,\n });\n return true;\n } else {\n log.debug(\"Domain allowed but path not matched\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n });\n }\n } else {\n log.info(\"Domain allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return true;\n }\n }\n }\n\n // Domain not in allow list\n log.info(\"Domain not in allow list, filtering out\", { domain, url });\n return false;\n}\n","/**\n * Sensitive header patterns and redaction configuration\n */\n\n/**\n * Default patterns for sensitive headers that should be redacted\n * These are matched case-insensitively\n */\nexport const DEFAULT_SENSITIVE_HEADER_PATTERNS = [\n // Authentication & Authorization\n \"authorization\",\n \"www-authenticate\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"x-auth-token\",\n \"x-api-key\",\n \"x-api-token\",\n \"x-access-token\",\n \"x-auth-user\",\n \"x-auth-password\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n\n // API Keys & Access Tokens\n \"api-key\",\n \"apikey\",\n \"api_key\",\n \"access-key\",\n \"accesskey\",\n \"access_key\",\n \"secret-key\",\n \"secretkey\",\n \"secret_key\",\n \"private-key\",\n \"privatekey\",\n \"private_key\",\n\n // Session & Cookie tokens\n \"cookie\",\n \"set-cookie\",\n \"session-id\",\n \"sessionid\",\n \"session_id\",\n \"session-token\",\n \"sessiontoken\",\n \"session_token\",\n\n // OAuth & OAuth2\n \"oauth-token\",\n \"oauth_token\",\n \"oauth2-token\",\n \"oauth2_token\",\n \"bearer\",\n\n // AWS & Cloud credentials\n \"x-amz-security-token\",\n \"x-amz-signature\",\n \"x-aws-access-key\",\n \"x-aws-secret-key\",\n \"x-aws-session-token\",\n\n // Other common sensitive headers\n \"x-password\",\n \"x-secret\",\n \"x-token\",\n \"x-jwt\",\n \"x-jwt-token\",\n \"x-refresh-token\",\n \"x-client-secret\",\n \"x-client-id\",\n \"x-user-token\",\n \"x-service-key\",\n] as const;\n\n/**\n * Redaction strategies for sensitive header values\n */\nexport enum HeaderRedactionStrategy {\n /**\n * Replace the entire value with a fixed redaction string\n */\n REPLACE = \"replace\",\n /**\n * Show only the first N characters, redact the rest\n */\n PARTIAL = \"partial\",\n /**\n * Show only the last N characters, redact the rest\n */\n PARTIAL_END = \"partial_end\",\n /**\n * Remove the header entirely (same as deny list)\n */\n REMOVE = \"remove\",\n}\n\n/**\n * Configuration for header redaction\n */\nexport interface HeaderRedactionConfig {\n /**\n * Patterns to match sensitive headers (case-insensitive)\n * Defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS if not provided\n */\n sensitivePatterns?: readonly string[];\n\n /**\n * Redaction strategy to use\n * @default HeaderRedactionStrategy.REPLACE\n */\n strategy?: HeaderRedactionStrategy;\n\n /**\n * Redaction string used when strategy is REPLACE\n * @default \"[REDACTED]\"\n */\n redactionString?: string;\n\n /**\n * Number of characters to show when strategy is PARTIAL or PARTIAL_END\n * @default 4\n */\n visibleChars?: number;\n\n /**\n * Whether to enable redaction\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Default redaction configuration\n */\nexport const DEFAULT_REDACTION_CONFIG: Required<HeaderRedactionConfig> = {\n sensitivePatterns: DEFAULT_SENSITIVE_HEADER_PATTERNS,\n strategy: HeaderRedactionStrategy.REPLACE,\n redactionString: \"[REDACTED]\",\n visibleChars: 4,\n enabled: true,\n};\n\n/**\n * Checks if a header name matches any sensitive pattern\n * Uses case-insensitive matching with exact match, prefix/suffix, and substring matching\n *\n * @param headerName - The header name to check\n * @param patterns - Array of patterns to match against (defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS)\n * @returns true if the header matches any sensitive pattern\n */\nexport function isSensitiveHeader(\n headerName: string,\n patterns: readonly string[] = DEFAULT_SENSITIVE_HEADER_PATTERNS\n): boolean {\n if (!headerName || typeof headerName !== \"string\") {\n return false;\n }\n\n if (!patterns || patterns.length === 0) {\n return false;\n }\n\n const normalizedName = headerName.toLowerCase().trim();\n\n // Early return for empty string\n if (normalizedName.length === 0) {\n return false;\n }\n\n return patterns.some((pattern) => {\n if (!pattern || typeof pattern !== \"string\") {\n return false;\n }\n\n const normalizedPattern = pattern.toLowerCase().trim();\n\n // Empty pattern doesn't match\n if (normalizedPattern.length === 0) {\n return false;\n }\n\n // Exact match (most common case, check first)\n if (normalizedName === normalizedPattern) {\n return true;\n }\n\n // Check if header name contains the pattern (e.g., \"x-api-key\" contains \"api-key\")\n // This handles cases where patterns are embedded in header names\n if (normalizedName.includes(normalizedPattern)) {\n return true;\n }\n\n // Check if pattern contains the header name (for shorter patterns matching longer headers)\n // This is less common but handles edge cases\n if (normalizedPattern.includes(normalizedName)) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Redacts a header value based on the configuration\n */\nexport function redactHeaderValue(\n value: string | string[] | undefined,\n config: Required<HeaderRedactionConfig>\n): string | string[] | undefined {\n if (value === undefined || value === null) {\n return value;\n }\n\n // Handle array of values\n if (Array.isArray(value)) {\n return value.map((v) => redactSingleValue(v, config));\n }\n\n return redactSingleValue(value, config);\n}\n\n/**\n * Redacts a single string value based on the configured strategy\n *\n * @param value - The value to redact\n * @param config - Redaction configuration\n * @returns Redacted value\n */\nfunction redactSingleValue(\n value: string,\n config: Required<HeaderRedactionConfig>\n): string {\n // Validate input\n if (!value || typeof value !== \"string\") {\n return value;\n }\n\n // Ensure visibleChars is a positive integer\n const visibleChars = Math.max(0, Math.floor(config.visibleChars || 0));\n const trimmedValue = value.trim();\n\n // Handle empty or very short values\n if (trimmedValue.length === 0) {\n return config.redactionString;\n }\n\n switch (config.strategy) {\n case HeaderRedactionStrategy.REPLACE:\n return config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL:\n // Show first N characters, then redaction string\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return trimmedValue.substring(0, visibleChars) + config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL_END:\n // Show last N characters, with redaction string prefix\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return (\n config.redactionString +\n trimmedValue.substring(trimmedValue.length - visibleChars)\n );\n\n case HeaderRedactionStrategy.REMOVE:\n // This should be handled at the filter level, not here\n // But if we reach here, return redaction string as fallback\n return config.redactionString;\n\n default:\n // Unknown strategy - default to full redaction for safety\n return config.redactionString;\n }\n}\n","/**\n * Header filtering logic - applies allow/deny list rules and redaction\n */\n\nimport { createLogger } from \"../logger\";\nimport type { HeaderRedactionConfig } from \"./sensitive-headers\";\nimport {\n DEFAULT_REDACTION_CONFIG,\n isSensitiveHeader,\n redactHeaderValue,\n HeaderRedactionStrategy,\n} from \"./sensitive-headers\";\n\nconst log = createLogger(\"[PingOps HeaderFilter]\");\n\nfunction toHeaderString(value: unknown): string | undefined {\n if (typeof value === \"string\") return value;\n if (\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n return undefined;\n}\n\n/**\n * Normalizes header name to lowercase for case-insensitive matching\n */\nfunction normalizeHeaderName(name: string): string {\n return name.toLowerCase();\n}\n\n/**\n * Merges redaction config with defaults\n */\nfunction mergeRedactionConfig(\n config?: HeaderRedactionConfig\n): Required<HeaderRedactionConfig> {\n // If config is undefined, use default config (enabled by default)\n if (!config) {\n return DEFAULT_REDACTION_CONFIG;\n }\n\n // If explicitly disabled, return disabled config\n if (config.enabled === false) {\n return { ...DEFAULT_REDACTION_CONFIG, enabled: false };\n }\n\n // Otherwise, merge with defaults (enabled defaults to true)\n return {\n sensitivePatterns:\n config.sensitivePatterns ?? DEFAULT_REDACTION_CONFIG.sensitivePatterns,\n strategy: config.strategy ?? DEFAULT_REDACTION_CONFIG.strategy,\n redactionString:\n config.redactionString ?? DEFAULT_REDACTION_CONFIG.redactionString,\n visibleChars: config.visibleChars ?? DEFAULT_REDACTION_CONFIG.visibleChars,\n enabled: config.enabled ?? DEFAULT_REDACTION_CONFIG.enabled,\n };\n}\n\n/**\n * Filters headers based on allow/deny lists and applies redaction to sensitive headers\n * - Deny list always wins (if header is in deny list, exclude it)\n * - Allow list filters included headers (if specified, only include these)\n * - Sensitive headers are redacted after filtering (if redaction is enabled)\n * - Case-insensitive matching\n *\n * @param headers - Headers to filter\n * @param headersAllowList - Optional allow list of header names to include\n * @param headersDenyList - Optional deny list of header names to exclude\n * @param redactionConfig - Optional configuration for header value redaction\n * @returns Filtered and redacted headers\n */\nexport function filterHeaders(\n headers: Record<string, string | string[] | undefined>,\n headersAllowList?: string[],\n headersDenyList?: string[],\n redactionConfig?: HeaderRedactionConfig\n): Record<string, string | string[] | undefined> {\n const originalCount = Object.keys(headers).length;\n const redaction = mergeRedactionConfig(redactionConfig);\n\n log.debug(\"Filtering headers\", {\n originalHeaderCount: originalCount,\n hasAllowList: !!headersAllowList && headersAllowList.length > 0,\n hasDenyList: !!headersDenyList && headersDenyList.length > 0,\n allowListCount: headersAllowList?.length || 0,\n denyListCount: headersDenyList?.length || 0,\n redactionEnabled: redaction.enabled,\n redactionStrategy: redaction.strategy,\n });\n\n const normalizedDenyList = headersDenyList?.map(normalizeHeaderName) ?? [];\n const normalizedAllowList = headersAllowList?.map(normalizeHeaderName) ?? [];\n\n const filtered: Record<string, string | string[] | undefined> = {};\n const deniedHeaders: string[] = [];\n const excludedHeaders: string[] = [];\n const redactedHeaders: string[] = [];\n\n for (const [name, value] of Object.entries(headers)) {\n const normalizedName = normalizeHeaderName(name);\n\n // Deny list always wins\n if (normalizedDenyList.includes(normalizedName)) {\n deniedHeaders.push(name);\n log.debug(\"Header denied by deny list\", { headerName: name });\n continue;\n }\n\n // If allow list exists, only include headers in the list\n if (normalizedAllowList.length > 0) {\n if (!normalizedAllowList.includes(normalizedName)) {\n excludedHeaders.push(name);\n log.debug(\"Header excluded (not in allow list)\", { headerName: name });\n continue;\n }\n }\n\n // Apply redaction if enabled and header is sensitive\n let finalValue = value;\n if (redaction.enabled) {\n try {\n // Check if header matches sensitive patterns\n if (isSensitiveHeader(name, redaction.sensitivePatterns)) {\n // Handle REMOVE strategy at filter level\n if (redaction.strategy === HeaderRedactionStrategy.REMOVE) {\n log.debug(\"Header removed by redaction strategy\", {\n headerName: name,\n });\n continue;\n }\n\n // Redact the value\n finalValue = redactHeaderValue(value, redaction);\n redactedHeaders.push(name);\n log.debug(\"Header value redacted\", {\n headerName: name,\n strategy: redaction.strategy,\n });\n }\n } catch (error) {\n // Log error but don't fail - use original value as fallback\n log.warn(\"Error redacting header value\", {\n headerName: name,\n error: error instanceof Error ? error.message : String(error),\n });\n finalValue = value;\n }\n }\n\n filtered[name] = finalValue;\n }\n\n const filteredCount = Object.keys(filtered).length;\n log.info(\"Header filtering complete\", {\n originalCount,\n filteredCount,\n deniedCount: deniedHeaders.length,\n excludedCount: excludedHeaders.length,\n redactedCount: redactedHeaders.length,\n deniedHeaders: deniedHeaders.length > 0 ? deniedHeaders : undefined,\n excludedHeaders: excludedHeaders.length > 0 ? excludedHeaders : undefined,\n redactedHeaders: redactedHeaders.length > 0 ? redactedHeaders : undefined,\n });\n\n return filtered;\n}\n\n/**\n * Extracts and normalizes headers from OpenTelemetry span attributes\n *\n * Handles two formats:\n * 1. Flat array format (e.g., 'http.request.header.0', 'http.request.header.1')\n * - 'http.request.header.0': 'Content-Type'\n * - 'http.request.header.1': 'application/json'\n * 2. Direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n * - 'http.request.header.date': 'Mon, 12 Jan 2026 20:22:38 GMT'\n * - 'http.request.header.content-type': 'application/json'\n *\n * This function converts them to:\n * - { 'Content-Type': 'application/json', 'date': 'Mon, 12 Jan 2026 20:22:38 GMT' }\n */\nexport function extractHeadersFromAttributes(\n attributes: Record<string, unknown>,\n headerPrefix: \"http.request.header\" | \"http.response.header\"\n): Record<string, string | string[] | undefined> | null {\n const headerMap: Record<string, string | string[] | undefined> = {};\n const headerKeys: number[] = [];\n const directKeyValueHeaders: Array<{ key: string; headerName: string }> = [];\n\n const prefixPattern = `${headerPrefix}.`;\n const numericPattern = new RegExp(\n `^${headerPrefix.replace(/\\./g, \"\\\\.\")}\\\\.(\\\\d+)$`\n );\n\n // Find all keys matching the pattern\n for (const key in attributes) {\n if (key.startsWith(prefixPattern) && key !== headerPrefix) {\n // Check for numeric index format (flat array)\n const numericMatch = key.match(numericPattern);\n if (numericMatch) {\n const index = parseInt(numericMatch[1], 10);\n headerKeys.push(index);\n } else {\n // Check for direct key-value format (e.g., 'http.request.header.date')\n const headerName = key.substring(prefixPattern.length);\n if (headerName.length > 0) {\n directKeyValueHeaders.push({ key, headerName });\n }\n }\n }\n }\n\n // Process numeric index format (flat array)\n if (headerKeys.length > 0) {\n // Sort indices to process in order\n headerKeys.sort((a, b) => a - b);\n\n // Convert flat array to key-value pairs\n // Even indices are header names, odd indices are header values\n for (let i = 0; i < headerKeys.length; i += 2) {\n const nameIndex = headerKeys[i];\n const valueIndex = headerKeys[i + 1];\n\n if (valueIndex !== undefined) {\n const nameKey = `${headerPrefix}.${nameIndex}`;\n const valueKey = `${headerPrefix}.${valueIndex}`;\n\n const headerName = attributes[nameKey] as string | undefined;\n const headerValue = attributes[valueKey] as string | undefined;\n\n if (headerName && headerValue !== undefined) {\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, headerValue]\n : [existing as string, headerValue];\n } else {\n // Use original case for the first occurrence\n headerMap[headerName] = headerValue;\n }\n }\n }\n }\n }\n\n // Process direct key-value format (e.g., 'http.request.header.date')\n if (directKeyValueHeaders.length > 0) {\n for (const { key, headerName } of directKeyValueHeaders) {\n const headerValue = attributes[key];\n\n if (headerValue !== undefined && headerValue !== null) {\n // Convert to string if needed\n const stringValue = toHeaderString(headerValue);\n if (stringValue === undefined) {\n continue;\n }\n\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, stringValue]\n : [existing as string, stringValue];\n } else {\n // Use the header name as stored (may be lowercase from instrumentation)\n headerMap[headerName] = stringValue;\n }\n }\n }\n }\n\n return Object.keys(headerMap).length > 0 ? headerMap : null;\n}\n\n/**\n * Type guard to check if value is a Headers-like object\n */\nfunction isHeadersLike(\n headers: unknown\n): headers is { entries: () => IterableIterator<[string, string]> } {\n return (\n typeof headers === \"object\" &&\n headers !== null &&\n \"entries\" in headers &&\n typeof (headers as { entries?: unknown }).entries === \"function\"\n );\n}\n\n/**\n * Normalizes headers from various sources into a proper key-value object\n */\nexport function normalizeHeaders(\n headers: unknown\n): Record<string, string | string[] | undefined> {\n const result: Record<string, string | string[] | undefined> = {};\n\n if (!headers) {\n return result;\n }\n\n try {\n // Handle array first: arrays also expose entries(), so this must run\n // before the Headers-like branch.\n if (Array.isArray(headers)) {\n // Try to reconstruct from array pairs\n for (let i = 0; i < headers.length; i += 2) {\n if (i + 1 < headers.length) {\n const key = String(headers[i]);\n const value = headers[i + 1] as string | string[] | undefined;\n result[key] = value;\n }\n }\n return result;\n }\n\n // Handle Headers object (from fetch/undici)\n if (isHeadersLike(headers)) {\n for (const [key, value] of headers.entries()) {\n // Headers can have multiple values for the same key\n if (result[key]) {\n // Convert to array if not already\n const existing = result[key];\n result[key] = Array.isArray(existing)\n ? [...existing, value]\n : [existing, value];\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n // Handle plain object\n if (typeof headers === \"object\" && !Array.isArray(headers)) {\n for (const [key, value] of Object.entries(headers)) {\n // Skip numeric keys (array-like objects)\n if (!/^\\d+$/.test(key)) {\n result[key] = value as string | string[] | undefined;\n }\n }\n return result;\n }\n } catch {\n // Fail silently - return empty object\n }\n\n return result;\n}\n","/**\n * Minimal body handling: buffer to string for span attributes.\n * No decompression or truncation; for compressed responses the instrumentation\n * sends base64 + content-encoding so the backend can decompress.\n */\n\n/** Span attribute for response content-encoding when body is sent as base64. */\nexport const HTTP_RESPONSE_CONTENT_ENCODING = \"http.response.content_encoding\";\n\nconst COMPRESSED_ENCODINGS = new Set([\n \"gzip\",\n \"br\",\n \"deflate\",\n \"x-gzip\",\n \"x-deflate\",\n]);\n\nfunction safeStringify(value: unknown): string | undefined {\n if (typeof value === \"string\") return value;\n if (\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n return undefined;\n}\n\nfunction normalizeHeaderValue(v: unknown): string | undefined {\n if (v == null) return undefined;\n if (Array.isArray(v)) {\n const parts = v\n .map((item) => safeStringify(item))\n .filter((item): item is string => item !== undefined);\n const joined = parts.join(\", \").trim();\n return joined || undefined;\n }\n\n const s = safeStringify(v);\n if (!s) return undefined;\n return s.trim() || undefined;\n}\n\n/**\n * Returns true if the content-encoding header indicates a compressed body\n * (gzip, br, deflate, x-gzip, x-deflate). Used to decide whether to send\n * body as base64 + content-encoding for backend decompression.\n */\nexport function isCompressedContentEncoding(headerValue: unknown): boolean {\n const raw = normalizeHeaderValue(headerValue);\n if (!raw) return false;\n const first = raw.split(\",\")[0].trim().toLowerCase();\n return COMPRESSED_ENCODINGS.has(first);\n}\n\n/**\n * Converts a buffer to a UTF-8 string for use as request/response body on spans.\n * Returns null for null, undefined, or empty buffer.\n */\nexport function bufferToBodyString(\n buffer: Buffer | null | undefined\n): string | null {\n if (buffer == null || buffer.length === 0) return null;\n return buffer.toString(\"utf8\");\n}\n","/**\n * Extracts structured data from spans for PingOps backend\n */\n\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport type { DomainRule, SpanPayload } from \"../types\";\nimport type { HeaderRedactionConfig } from \"../filtering/sensitive-headers\";\nimport { getHttpUrlFromAttributes } from \"./http-attributes\";\nimport {\n filterHeaders,\n extractHeadersFromAttributes,\n} from \"../filtering/header-filter\";\n\n/**\n * Extracts domain from URL\n */\nfunction extractDomainFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n return match ? match[1] : \"\";\n }\n}\n\n/**\n * Gets domain rule configuration for a given URL\n */\nfunction getDomainRule(\n url: string,\n domainAllowList?: DomainRule[]\n): DomainRule | undefined {\n if (!domainAllowList) {\n return undefined;\n }\n\n const domain = extractDomainFromUrl(url);\n for (const rule of domainAllowList) {\n if (\n domain === rule.domain ||\n domain.endsWith(`.${rule.domain}`) ||\n domain === rule.domain.slice(1)\n ) {\n return rule;\n }\n }\n return undefined;\n}\n\n/**\n * Determines if body should be captured based on priority:\n * domain rule > global config > default (false)\n */\nfunction shouldCaptureBody(\n domainRule: DomainRule | undefined,\n globalConfig: boolean | undefined,\n bodyType: \"request\" | \"response\"\n): boolean {\n // Check domain-specific rule first\n if (domainRule) {\n const domainValue =\n bodyType === \"request\"\n ? domainRule.captureRequestBody\n : domainRule.captureResponseBody;\n if (domainValue !== undefined) {\n return domainValue;\n }\n }\n // Fall back to global config\n if (globalConfig !== undefined) {\n return globalConfig;\n }\n // Default to false\n return false;\n}\n\n/**\n * Extracts structured payload from a span\n */\nexport function extractSpanPayload(\n span: ReadableSpan,\n domainAllowList?: DomainRule[],\n globalHeadersAllowList?: string[],\n globalHeadersDenyList?: string[],\n globalCaptureRequestBody?: boolean,\n globalCaptureResponseBody?: boolean,\n headerRedaction?: HeaderRedactionConfig\n): SpanPayload | null {\n const attributes = span.attributes;\n const url = getHttpUrlFromAttributes(attributes);\n\n // Get domain-specific rule if available\n const domainRule = url ? getDomainRule(url, domainAllowList) : undefined;\n\n // Merge global and domain-specific header rules\n const headersAllowList =\n domainRule?.headersAllowList ?? globalHeadersAllowList;\n const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;\n\n // Determine if bodies should be captured\n const shouldCaptureReqBody = shouldCaptureBody(\n domainRule,\n globalCaptureRequestBody,\n \"request\"\n );\n const shouldCaptureRespBody = shouldCaptureBody(\n domainRule,\n globalCaptureResponseBody,\n \"response\"\n );\n\n // Extract HTTP headers if available\n let requestHeaders: Record<string, string | string[] | undefined> = {};\n let responseHeaders: Record<string, string | string[] | undefined> = {};\n\n // First, try to extract flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1')\n // or direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n const flatRequestHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.request.header\"\n );\n const flatResponseHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.response.header\"\n );\n\n // Try to get headers from attributes (format may vary by instrumentation)\n const httpRequestHeadersValue = attributes[\"http.request.header\"];\n const httpResponseHeadersValue = attributes[\"http.response.header\"];\n\n // Type guard: check if value is a record/object with string keys\n const isHeadersRecord = (\n value: unknown\n ): value is Record<string, string | string[] | undefined> => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value).every(\n (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((item) => typeof item === \"string\")) ||\n v === undefined\n )\n );\n };\n\n // Use flat array format if available, otherwise use direct attribute\n if (flatRequestHeaders) {\n requestHeaders = filterHeaders(\n flatRequestHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpRequestHeadersValue)) {\n requestHeaders = filterHeaders(\n httpRequestHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n if (flatResponseHeaders) {\n responseHeaders = filterHeaders(\n flatResponseHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpResponseHeadersValue)) {\n responseHeaders = filterHeaders(\n httpResponseHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n // Build attributes object\n const extractedAttributes: Record<string, unknown> = {\n ...attributes,\n };\n\n // Remove flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1', etc.)\n // and direct key-value format headers (e.g., 'http.request.header.date', 'http.request.header.content-type')\n // We'll replace them with the proper key-value format\n for (const key in extractedAttributes) {\n if (\n (key.startsWith(\"http.request.header.\") &&\n key !== \"http.request.header\") ||\n (key.startsWith(\"http.response.header.\") &&\n key !== \"http.response.header\")\n ) {\n // Remove both numeric index format and direct key-value format\n delete extractedAttributes[key];\n }\n }\n\n // Add filtered headers in proper key-value format\n if (Object.keys(requestHeaders).length > 0) {\n extractedAttributes[\"http.request.header\"] = requestHeaders;\n }\n\n if (Object.keys(responseHeaders).length > 0) {\n extractedAttributes[\"http.response.header\"] = responseHeaders;\n }\n\n // Remove body attributes if capture is disabled\n if (!shouldCaptureReqBody) {\n delete extractedAttributes[\"http.request.body\"];\n }\n\n if (!shouldCaptureRespBody) {\n delete extractedAttributes[\"http.response.body\"];\n }\n\n // Build span payload\n const spanContext = span.spanContext();\n // parentSpanId may not be available in all versions of ReadableSpan\n const parentSpanId =\n \"parentSpanId\" in span\n ? (span as ReadableSpan & { parentSpanId?: string }).parentSpanId\n : undefined;\n return {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n parentSpanId,\n name: span.name,\n kind: span.kind.toString(),\n startTime: new Date(\n span.startTime[0] * 1000 + span.startTime[1] / 1000000\n ).toISOString(),\n endTime: new Date(\n span.endTime[0] * 1000 + span.endTime[1] / 1000000\n ).toISOString(),\n duration:\n (span.endTime[0] - span.startTime[0]) * 1000 +\n (span.endTime[1] - span.startTime[1]) / 1000000,\n attributes: extractedAttributes,\n status: {\n code: span.status.code.toString(),\n message: span.status.message,\n },\n };\n}\n","/**\n * OpenTelemetry context keys for PingOps\n */\n\nimport { createContextKey } from \"@opentelemetry/api\";\n\n/**\n * Context key for trace ID attribute.\n * Used to propagate trace identifier to all spans in the context.\n */\nexport const PINGOPS_TRACE_ID = createContextKey(\"pingops-trace-id\");\n\n/**\n * Context key for user ID attribute.\n * Used to propagate user identifier to all spans in the context.\n */\nexport const PINGOPS_USER_ID = createContextKey(\"pingops-user-id\");\n\n/**\n * Context key for session ID attribute.\n * Used to propagate session identifier to all spans in the context.\n */\nexport const PINGOPS_SESSION_ID = createContextKey(\"pingops-session-id\");\n\n/**\n * Context key for tags attribute.\n * Used to propagate tags array to all spans in the context.\n */\nexport const PINGOPS_TAGS = createContextKey(\"pingops-tags\");\n\n/**\n * Context key for metadata attribute.\n * Used to propagate metadata object to all spans in the context.\n */\nexport const PINGOPS_METADATA = createContextKey(\"pingops-metadata\");\n\n/**\n * Context key for capturing request body.\n * When set, controls whether request bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_REQUEST_BODY = createContextKey(\n \"pingops-capture-request-body\"\n);\n\n/**\n * Context key for capturing response body.\n * When set, controls whether response bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_RESPONSE_BODY = createContextKey(\n \"pingops-capture-response-body\"\n);\n","/**\n * Extracts propagated attributes from OpenTelemetry context\n */\n\nimport type { Context } from \"@opentelemetry/api\";\nimport {\n PINGOPS_TRACE_ID,\n PINGOPS_USER_ID,\n PINGOPS_SESSION_ID,\n PINGOPS_TAGS,\n PINGOPS_METADATA,\n} from \"../context-keys\";\n\n/**\n * Extracts propagated attributes from the given context and returns them\n * as span attributes that can be set on a span.\n *\n * @param parentContext - The OpenTelemetry context to extract attributes from\n * @returns Record of attribute key-value pairs to set on spans\n */\nexport function getPropagatedAttributesFromContext(\n parentContext: Context\n): Record<string, string | string[]> {\n const attributes: Record<string, string | string[]> = {};\n\n // Extract traceId\n const traceId = parentContext.getValue(PINGOPS_TRACE_ID);\n if (traceId !== undefined && typeof traceId === \"string\") {\n attributes[\"pingops.trace_id\"] = traceId;\n }\n\n // Extract userId\n const userId = parentContext.getValue(PINGOPS_USER_ID);\n if (userId !== undefined && typeof userId === \"string\") {\n attributes[\"pingops.user_id\"] = userId;\n }\n\n // Extract sessionId\n const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);\n if (sessionId !== undefined && typeof sessionId === \"string\") {\n attributes[\"pingops.session_id\"] = sessionId;\n }\n\n // Extract tags\n const tags = parentContext.getValue(PINGOPS_TAGS);\n if (tags !== undefined && Array.isArray(tags)) {\n attributes[\"pingops.tags\"] = tags;\n }\n\n // Extract metadata\n const metadata = parentContext.getValue(PINGOPS_METADATA);\n if (\n metadata !== undefined &&\n typeof metadata === \"object\" &&\n metadata !== null &&\n !Array.isArray(metadata)\n ) {\n // Flatten metadata object into span attributes with prefix\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n attributes[`pingops.metadata.${key}`] = value;\n }\n }\n }\n\n return attributes;\n}\n","/**\n * Deterministic and random trace ID generation for PingOps\n */\n\n/**\n * Converts a Uint8Array to a lowercase hex string.\n */\nexport function uint8ArrayToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Creates a trace ID (32 hex chars).\n * - If `seed` is provided: deterministic via SHA-256 of the seed (first 32 hex chars).\n * - Otherwise: random 16 bytes as 32 hex chars.\n */\nexport async function createTraceId(seed?: string): Promise<string> {\n if (seed) {\n const data = new TextEncoder().encode(seed);\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const hashArray = new Uint8Array(hashBuffer);\n return uint8ArrayToHex(hashArray).slice(0, 32);\n }\n\n const randomValues = crypto.getRandomValues(new Uint8Array(16));\n return uint8ArrayToHex(randomValues);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pingops/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20"
|
|
@@ -36,11 +36,13 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^20.11.0",
|
|
39
|
-
"typescript": "^5.6.0"
|
|
39
|
+
"typescript": "^5.6.0",
|
|
40
|
+
"vitest": "^3.2.4"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"build": "tsdown",
|
|
43
44
|
"dev": "tsdown --watch",
|
|
44
|
-
"clean": "rm -rf dist"
|
|
45
|
+
"clean": "rm -rf dist",
|
|
46
|
+
"test": "vitest run"
|
|
45
47
|
}
|
|
46
48
|
}
|