apitally 1.0.0-beta.4 → 1.0.0-beta.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.cjs","names":["millisToHrTime","ValueType","AggregationTemporality","DataPointType","ProtobufMetricsSerializer","availableParallelism"],"sources":["../src/metrics.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { type Attributes, ValueType } from \"@opentelemetry/api\";\nimport { millisToHrTime } from \"@opentelemetry/core\";\nimport { ProtobufMetricsSerializer } from \"@opentelemetry/otlp-transformer\";\nimport type { Resource } from \"@opentelemetry/resources\";\nimport {\n AggregationTemporality,\n DataPointType,\n type ExponentialHistogramMetricData,\n type MetricData,\n} from \"@opentelemetry/sdk-metrics\";\nimport type { RequestRecord } from \"./context.js\";\nimport { logWarning } from \"./logger.js\";\nimport type { Spool } from \"./spool.js\";\n\nconst HISTOGRAM_SCALE = 3;\nconst SCALE_FACTOR = Math.LOG2E * 2 ** HISTOGRAM_SCALE;\n// Guards against misuse such as a request ID used as the consumer identifier.\nconst MAX_COMBINATIONS = 50_000;\n// Keeps each appended request well below the spool's 4 MB file rotation threshold.\nconst COMBINATIONS_PER_REQUEST = 1_000;\nconst CAPACITY_WARNING = `Apitally request metrics exceeded the capacity of ${MAX_COMBINATIONS.toLocaleString(\"en-US\")} distinct attribute combinations per collection interval, so some request metrics are missing.`;\nconst HISTOGRAM_METRICS = [\n [\"http.server.request.duration\", \"s\", \"duration\"],\n [\"http.server.request.body.size\", \"By\", \"requestBodySize\"],\n [\"http.server.response.body.size\", \"By\", \"responseBodySize\"],\n] as const;\n\ninterface RequestHistograms {\n attributes: Attributes;\n duration: ExponentialHistogram;\n requestBodySize: ExponentialHistogram;\n responseBodySize: ExponentialHistogram;\n}\n\n// Request histograms use finalized transport data, independent of span timing and sampling.\nexport class MetricsPipeline {\n private readonly resource: Resource;\n private readonly spool: Spool;\n private combinations = new Map<string, RequestHistograms>();\n private intervalStartTime = millisToHrTime(Date.now());\n private lastCpuUsage = process.cpuUsage();\n private lastCpuUsageTimeMillis = performance.now();\n\n constructor(resource: Resource, spool: Spool) {\n this.resource = resource;\n this.spool = spool;\n }\n\n // Excluded and sampled-out requests count; preflight, websocket, and unmatched routes do not.\n recordFromRequest(record: RequestRecord): void {\n if (record.dropReason === \"method\" || record.dropReason === \"scheme\") {\n return;\n }\n const source = record.attributes;\n const method = source[\"http.request.method\"] ?? source[\"http.method\"];\n const scheme = source[\"url.scheme\"] ?? source[\"http.scheme\"];\n const route = source[\"http.route\"];\n if (typeof route !== \"string\" || route === \"\") {\n return;\n }\n const attributes: Attributes = { \"http.route\": route };\n if (method !== undefined) {\n attributes[\"http.request.method\"] = method;\n }\n const statusCode = source[\"http.response.status_code\"] ?? source[\"http.status_code\"];\n if (statusCode !== undefined) {\n attributes[\"http.response.status_code\"] = statusCode;\n }\n const consumer = source[\"apitally.consumer.identifier\"];\n if (consumer !== undefined) {\n attributes[\"apitally.consumer.identifier\"] = consumer;\n }\n if (scheme !== undefined) {\n attributes[\"url.scheme\"] = scheme;\n }\n if (typeof statusCode === \"number\" && statusCode >= 500) {\n attributes[\"error.type\"] = String(statusCode);\n }\n // Attributes are always set in the same order, so equal combinations give equal keys.\n const key = JSON.stringify(attributes);\n let histograms = this.combinations.get(key);\n if (histograms === undefined) {\n if (this.combinations.size >= MAX_COMBINATIONS) {\n logWarning(CAPACITY_WARNING);\n return;\n }\n histograms = {\n attributes,\n duration: new ExponentialHistogram(),\n requestBodySize: new ExponentialHistogram(),\n responseBodySize: new ExponentialHistogram(),\n };\n this.combinations.set(key, histograms);\n }\n if (typeof record.durationSeconds === \"number\") {\n histograms.duration.record(record.durationSeconds);\n }\n const requestBodySize = source[\"http.request.body.size\"];\n if (typeof requestBodySize === \"number\") {\n histograms.requestBodySize.record(requestBodySize);\n }\n const responseBodySize = source[\"http.response.body.size\"];\n if (typeof responseBodySize === \"number\") {\n histograms.responseBodySize.record(responseBodySize);\n }\n }\n\n async collectAndExport(): Promise<void> {\n const combinations = [...this.combinations.values()];\n this.combinations = new Map();\n const startTime = this.intervalStartTime;\n const endTime = millisToHrTime(Date.now());\n this.intervalStartTime = endTime;\n const gauges: [string, string, number][] = [\n [\"process.cpu.utilization\", \"1\", this.observeCpuUtilization()],\n [\"process.memory.usage\", \"By\", process.memoryUsage.rss()],\n [\"process.uptime\", \"s\", process.uptime()],\n ];\n // Apitally ingest treats every metrics export as a liveness signal, so the\n // gauges are appended on every collection.\n await this.append(\n gauges.map(([name, unit, value]) => ({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.CUMULATIVE,\n dataPointType: DataPointType.GAUGE,\n dataPoints: [{ startTime, endTime, attributes: {}, value }],\n })),\n );\n // Each combination's histograms stay in one request because Apitally ingest\n // joins them per resource entry.\n for (let index = 0; index < combinations.length; index += COMBINATIONS_PER_REQUEST) {\n const slice = combinations.slice(index, index + COMBINATIONS_PER_REQUEST);\n const metrics: ExponentialHistogramMetricData[] = [];\n for (const [name, unit, field] of HISTOGRAM_METRICS) {\n const dataPoints = slice\n .filter((histograms) => histograms[field].count > 0)\n .map(({ attributes, [field]: histogram }) => ({\n startTime,\n endTime,\n attributes,\n value: {\n count: histogram.count,\n sum: histogram.sum,\n min: histogram.min,\n max: histogram.max,\n scale: HISTOGRAM_SCALE,\n zeroCount: histogram.zeroCount,\n positive: { offset: histogram.offset, bucketCounts: histogram.bucketCounts },\n negative: { offset: 0, bucketCounts: [] },\n },\n }));\n if (dataPoints.length > 0) {\n metrics.push({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.DELTA,\n dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM,\n dataPoints,\n });\n }\n }\n await this.append(metrics);\n }\n }\n\n private async append(metrics: MetricData[]): Promise<void> {\n const payload = ProtobufMetricsSerializer.serializeRequest({\n resource: this.resource,\n scopeMetrics: [{ scope: { name: \"apitally\" }, metrics }],\n });\n if (payload) {\n await this.spool.append(\"metrics\", payload);\n }\n }\n\n private observeCpuUtilization(): number {\n const cpuUsage = process.cpuUsage();\n const nowMillis = performance.now();\n const cpuTimeMicros =\n cpuUsage.user - this.lastCpuUsage.user + (cpuUsage.system - this.lastCpuUsage.system);\n const elapsedMicros = (nowMillis - this.lastCpuUsageTimeMillis) * 1000;\n this.lastCpuUsage = cpuUsage;\n this.lastCpuUsageTimeMillis = nowMillis;\n return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;\n }\n}\n\n// Base-2 exponential histogram at a fixed scale for non-negative values, with the\n// bucket indexes of the OpenTelemetry specification.\nclass ExponentialHistogram {\n count = 0;\n sum = 0;\n min = Number.POSITIVE_INFINITY;\n max = Number.NEGATIVE_INFINITY;\n zeroCount = 0;\n offset = 0;\n bucketCounts: number[] = [];\n\n record(value: number): void {\n this.count++;\n this.sum += value;\n this.min = Math.min(this.min, value);\n this.max = Math.max(this.max, value);\n if (value === 0) {\n this.zeroCount++;\n return;\n }\n const index = mapToIndex(value);\n if (this.bucketCounts.length === 0) {\n this.offset = index;\n } else if (index < this.offset) {\n this.bucketCounts.unshift(...new Array<number>(this.offset - index).fill(0));\n this.offset = index;\n }\n const position = index - this.offset;\n while (this.bucketCounts.length <= position) {\n this.bucketCounts.push(0);\n }\n this.bucketCounts[position]++;\n }\n}\n\nfunction mapToIndex(value: number): number {\n const exponent = Math.floor(Math.log2(value));\n // Exact powers of two are the inclusive upper boundary of the bucket below.\n if (2 ** exponent === value) {\n return (exponent << HISTOGRAM_SCALE) - 1;\n }\n return Math.floor(Math.log(value) * SCALE_FACTOR);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.cjs","names":["millisToHrTime","ValueType","AggregationTemporality","DataPointType","ProtobufMetricsSerializer","availableParallelism"],"sources":["../src/metrics.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { type Attributes, ValueType } from \"@opentelemetry/api\";\nimport { millisToHrTime } from \"@opentelemetry/core\";\nimport { ProtobufMetricsSerializer } from \"@opentelemetry/otlp-transformer\";\nimport type { Resource } from \"@opentelemetry/resources\";\nimport {\n AggregationTemporality,\n DataPointType,\n type ExponentialHistogramMetricData,\n type MetricData,\n} from \"@opentelemetry/sdk-metrics\";\nimport type { RequestRecord } from \"./context.js\";\nimport { logWarning } from \"./logger.js\";\nimport type { Spool } from \"./spool.js\";\n\nconst HISTOGRAM_SCALE = 3;\nconst SCALE_FACTOR = Math.LOG2E * 2 ** HISTOGRAM_SCALE;\n// Guards against misuse such as a request ID used as the consumer identifier.\nconst MAX_COMBINATIONS = 50_000;\n// Keeps each appended request well below the spool's 4 MB file rotation threshold.\nconst COMBINATIONS_PER_REQUEST = 1_000;\nconst CAPACITY_WARNING = `Apitally request metrics exceeded the capacity of ${MAX_COMBINATIONS.toLocaleString(\"en-US\")} distinct attribute combinations per collection interval, so some request metrics are missing.`;\nconst HISTOGRAM_METRICS = [\n [\"http.server.request.duration\", \"s\", \"duration\"],\n [\"http.server.request.body.size\", \"By\", \"requestBodySize\"],\n [\"http.server.response.body.size\", \"By\", \"responseBodySize\"],\n] as const;\n\ninterface RequestHistograms {\n attributes: Attributes;\n duration: ExponentialHistogram;\n requestBodySize: ExponentialHistogram;\n responseBodySize: ExponentialHistogram;\n}\n\n// Request histograms use finalized transport data, independent of span timing and sampling.\nexport class MetricsPipeline {\n private readonly resource: Resource;\n private readonly spool: Spool;\n private combinations = new Map<string, RequestHistograms>();\n private intervalStartTime = millisToHrTime(Date.now());\n private lastCpuUsage = process.cpuUsage();\n private lastCpuUsageTimeMillis = performance.now();\n\n constructor(resource: Resource, spool: Spool) {\n this.resource = resource;\n this.spool = spool;\n }\n\n // Excluded and sampled-out requests count; preflight, websocket, and unmatched routes do not.\n recordFromRequest(record: RequestRecord): void {\n if (record.dropReason === \"method\" || record.dropReason === \"scheme\") {\n return;\n }\n const source = record.attributes;\n const method = source[\"http.request.method\"] ?? source[\"http.method\"];\n const route = source[\"http.route\"];\n if (typeof route !== \"string\" || route === \"\") {\n return;\n }\n const attributes: Attributes = { \"http.route\": route };\n if (method !== undefined) {\n attributes[\"http.request.method\"] = method;\n }\n const statusCode = source[\"http.response.status_code\"] ?? source[\"http.status_code\"];\n if (statusCode !== undefined) {\n attributes[\"http.response.status_code\"] = statusCode;\n }\n const consumer = source[\"apitally.consumer.identifier\"];\n if (consumer !== undefined) {\n attributes[\"apitally.consumer.identifier\"] = consumer;\n }\n // Attributes are always set in the same order, so equal combinations give equal keys.\n const key = JSON.stringify(attributes);\n let histograms = this.combinations.get(key);\n if (histograms === undefined) {\n if (this.combinations.size >= MAX_COMBINATIONS) {\n logWarning(CAPACITY_WARNING);\n return;\n }\n histograms = {\n attributes,\n duration: new ExponentialHistogram(),\n requestBodySize: new ExponentialHistogram(),\n responseBodySize: new ExponentialHistogram(),\n };\n this.combinations.set(key, histograms);\n }\n if (typeof record.durationSeconds === \"number\") {\n histograms.duration.record(record.durationSeconds);\n }\n const requestBodySize = source[\"http.request.body.size\"];\n if (typeof requestBodySize === \"number\") {\n histograms.requestBodySize.record(requestBodySize);\n }\n const responseBodySize = source[\"http.response.body.size\"];\n if (typeof responseBodySize === \"number\") {\n histograms.responseBodySize.record(responseBodySize);\n }\n }\n\n async collectAndExport(): Promise<void> {\n const combinations = [...this.combinations.values()];\n this.combinations = new Map();\n const startTime = this.intervalStartTime;\n const endTime = millisToHrTime(Date.now());\n this.intervalStartTime = endTime;\n const gauges: [string, string, number][] = [\n [\"process.cpu.utilization\", \"1\", this.observeCpuUtilization()],\n [\"process.memory.usage\", \"By\", process.memoryUsage.rss()],\n [\"process.uptime\", \"s\", process.uptime()],\n ];\n // Apitally ingest treats every metrics export as a liveness signal, so the\n // gauges are appended on every collection.\n await this.append(\n gauges.map(([name, unit, value]) => ({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.CUMULATIVE,\n dataPointType: DataPointType.GAUGE,\n dataPoints: [{ startTime, endTime, attributes: {}, value }],\n })),\n );\n // Each combination's histograms stay in one request because Apitally ingest\n // joins them per resource entry.\n for (let index = 0; index < combinations.length; index += COMBINATIONS_PER_REQUEST) {\n const slice = combinations.slice(index, index + COMBINATIONS_PER_REQUEST);\n const metrics: ExponentialHistogramMetricData[] = [];\n for (const [name, unit, field] of HISTOGRAM_METRICS) {\n const dataPoints = slice\n .filter((histograms) => histograms[field].count > 0)\n .map(({ attributes, [field]: histogram }) => ({\n startTime,\n endTime,\n attributes,\n value: {\n count: histogram.count,\n sum: histogram.sum,\n min: histogram.min,\n max: histogram.max,\n scale: HISTOGRAM_SCALE,\n zeroCount: histogram.zeroCount,\n positive: { offset: histogram.offset, bucketCounts: histogram.bucketCounts },\n negative: { offset: 0, bucketCounts: [] },\n },\n }));\n if (dataPoints.length > 0) {\n metrics.push({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.DELTA,\n dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM,\n dataPoints,\n });\n }\n }\n await this.append(metrics);\n }\n }\n\n private async append(metrics: MetricData[]): Promise<void> {\n const payload = ProtobufMetricsSerializer.serializeRequest({\n resource: this.resource,\n scopeMetrics: [{ scope: { name: \"apitally\" }, metrics }],\n });\n if (payload) {\n await this.spool.append(\"metrics\", payload);\n }\n }\n\n private observeCpuUtilization(): number {\n const cpuUsage = process.cpuUsage();\n const nowMillis = performance.now();\n const cpuTimeMicros =\n cpuUsage.user - this.lastCpuUsage.user + (cpuUsage.system - this.lastCpuUsage.system);\n const elapsedMicros = (nowMillis - this.lastCpuUsageTimeMillis) * 1000;\n this.lastCpuUsage = cpuUsage;\n this.lastCpuUsageTimeMillis = nowMillis;\n return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;\n }\n}\n\n// Base-2 exponential histogram at a fixed scale for non-negative values, with the\n// bucket indexes of the OpenTelemetry specification.\nclass ExponentialHistogram {\n count = 0;\n sum = 0;\n min = Number.POSITIVE_INFINITY;\n max = Number.NEGATIVE_INFINITY;\n zeroCount = 0;\n offset = 0;\n bucketCounts: number[] = [];\n\n record(value: number): void {\n this.count++;\n this.sum += value;\n this.min = Math.min(this.min, value);\n this.max = Math.max(this.max, value);\n if (value === 0) {\n this.zeroCount++;\n return;\n }\n const index = mapToIndex(value);\n if (this.bucketCounts.length === 0) {\n this.offset = index;\n } else if (index < this.offset) {\n this.bucketCounts.unshift(...new Array<number>(this.offset - index).fill(0));\n this.offset = index;\n }\n const position = index - this.offset;\n while (this.bucketCounts.length <= position) {\n this.bucketCounts.push(0);\n }\n this.bucketCounts[position]++;\n }\n}\n\nfunction mapToIndex(value: number): number {\n const exponent = Math.floor(Math.log2(value));\n // Exact powers of two are the inclusive upper boundary of the bucket below.\n if (2 ** exponent === value) {\n return (exponent << HISTOGRAM_SCALE) - 1;\n }\n return Math.floor(Math.log(value) * SCALE_FACTOR);\n}\n"],"mappings":"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"}
package/dist/metrics.js CHANGED
@@ -42,7 +42,6 @@ var MetricsPipeline = class {
42
42
  if (record.dropReason === "method" || record.dropReason === "scheme") return;
43
43
  const source = record.attributes;
44
44
  const method = source["http.request.method"] ?? source["http.method"];
45
- const scheme = source["url.scheme"] ?? source["http.scheme"];
46
45
  const route = source["http.route"];
47
46
  if (typeof route !== "string" || route === "") return;
48
47
  const attributes = { "http.route": route };
@@ -51,8 +50,6 @@ var MetricsPipeline = class {
51
50
  if (statusCode !== void 0) attributes["http.response.status_code"] = statusCode;
52
51
  const consumer = source["apitally.consumer.identifier"];
53
52
  if (consumer !== void 0) attributes["apitally.consumer.identifier"] = consumer;
54
- if (scheme !== void 0) attributes["url.scheme"] = scheme;
55
- if (typeof statusCode === "number" && statusCode >= 500) attributes["error.type"] = String(statusCode);
56
53
  const key = JSON.stringify(attributes);
57
54
  let histograms = this.combinations.get(key);
58
55
  if (histograms === void 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { type Attributes, ValueType } from \"@opentelemetry/api\";\nimport { millisToHrTime } from \"@opentelemetry/core\";\nimport { ProtobufMetricsSerializer } from \"@opentelemetry/otlp-transformer\";\nimport type { Resource } from \"@opentelemetry/resources\";\nimport {\n AggregationTemporality,\n DataPointType,\n type ExponentialHistogramMetricData,\n type MetricData,\n} from \"@opentelemetry/sdk-metrics\";\nimport type { RequestRecord } from \"./context.js\";\nimport { logWarning } from \"./logger.js\";\nimport type { Spool } from \"./spool.js\";\n\nconst HISTOGRAM_SCALE = 3;\nconst SCALE_FACTOR = Math.LOG2E * 2 ** HISTOGRAM_SCALE;\n// Guards against misuse such as a request ID used as the consumer identifier.\nconst MAX_COMBINATIONS = 50_000;\n// Keeps each appended request well below the spool's 4 MB file rotation threshold.\nconst COMBINATIONS_PER_REQUEST = 1_000;\nconst CAPACITY_WARNING = `Apitally request metrics exceeded the capacity of ${MAX_COMBINATIONS.toLocaleString(\"en-US\")} distinct attribute combinations per collection interval, so some request metrics are missing.`;\nconst HISTOGRAM_METRICS = [\n [\"http.server.request.duration\", \"s\", \"duration\"],\n [\"http.server.request.body.size\", \"By\", \"requestBodySize\"],\n [\"http.server.response.body.size\", \"By\", \"responseBodySize\"],\n] as const;\n\ninterface RequestHistograms {\n attributes: Attributes;\n duration: ExponentialHistogram;\n requestBodySize: ExponentialHistogram;\n responseBodySize: ExponentialHistogram;\n}\n\n// Request histograms use finalized transport data, independent of span timing and sampling.\nexport class MetricsPipeline {\n private readonly resource: Resource;\n private readonly spool: Spool;\n private combinations = new Map<string, RequestHistograms>();\n private intervalStartTime = millisToHrTime(Date.now());\n private lastCpuUsage = process.cpuUsage();\n private lastCpuUsageTimeMillis = performance.now();\n\n constructor(resource: Resource, spool: Spool) {\n this.resource = resource;\n this.spool = spool;\n }\n\n // Excluded and sampled-out requests count; preflight, websocket, and unmatched routes do not.\n recordFromRequest(record: RequestRecord): void {\n if (record.dropReason === \"method\" || record.dropReason === \"scheme\") {\n return;\n }\n const source = record.attributes;\n const method = source[\"http.request.method\"] ?? source[\"http.method\"];\n const scheme = source[\"url.scheme\"] ?? source[\"http.scheme\"];\n const route = source[\"http.route\"];\n if (typeof route !== \"string\" || route === \"\") {\n return;\n }\n const attributes: Attributes = { \"http.route\": route };\n if (method !== undefined) {\n attributes[\"http.request.method\"] = method;\n }\n const statusCode = source[\"http.response.status_code\"] ?? source[\"http.status_code\"];\n if (statusCode !== undefined) {\n attributes[\"http.response.status_code\"] = statusCode;\n }\n const consumer = source[\"apitally.consumer.identifier\"];\n if (consumer !== undefined) {\n attributes[\"apitally.consumer.identifier\"] = consumer;\n }\n if (scheme !== undefined) {\n attributes[\"url.scheme\"] = scheme;\n }\n if (typeof statusCode === \"number\" && statusCode >= 500) {\n attributes[\"error.type\"] = String(statusCode);\n }\n // Attributes are always set in the same order, so equal combinations give equal keys.\n const key = JSON.stringify(attributes);\n let histograms = this.combinations.get(key);\n if (histograms === undefined) {\n if (this.combinations.size >= MAX_COMBINATIONS) {\n logWarning(CAPACITY_WARNING);\n return;\n }\n histograms = {\n attributes,\n duration: new ExponentialHistogram(),\n requestBodySize: new ExponentialHistogram(),\n responseBodySize: new ExponentialHistogram(),\n };\n this.combinations.set(key, histograms);\n }\n if (typeof record.durationSeconds === \"number\") {\n histograms.duration.record(record.durationSeconds);\n }\n const requestBodySize = source[\"http.request.body.size\"];\n if (typeof requestBodySize === \"number\") {\n histograms.requestBodySize.record(requestBodySize);\n }\n const responseBodySize = source[\"http.response.body.size\"];\n if (typeof responseBodySize === \"number\") {\n histograms.responseBodySize.record(responseBodySize);\n }\n }\n\n async collectAndExport(): Promise<void> {\n const combinations = [...this.combinations.values()];\n this.combinations = new Map();\n const startTime = this.intervalStartTime;\n const endTime = millisToHrTime(Date.now());\n this.intervalStartTime = endTime;\n const gauges: [string, string, number][] = [\n [\"process.cpu.utilization\", \"1\", this.observeCpuUtilization()],\n [\"process.memory.usage\", \"By\", process.memoryUsage.rss()],\n [\"process.uptime\", \"s\", process.uptime()],\n ];\n // Apitally ingest treats every metrics export as a liveness signal, so the\n // gauges are appended on every collection.\n await this.append(\n gauges.map(([name, unit, value]) => ({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.CUMULATIVE,\n dataPointType: DataPointType.GAUGE,\n dataPoints: [{ startTime, endTime, attributes: {}, value }],\n })),\n );\n // Each combination's histograms stay in one request because Apitally ingest\n // joins them per resource entry.\n for (let index = 0; index < combinations.length; index += COMBINATIONS_PER_REQUEST) {\n const slice = combinations.slice(index, index + COMBINATIONS_PER_REQUEST);\n const metrics: ExponentialHistogramMetricData[] = [];\n for (const [name, unit, field] of HISTOGRAM_METRICS) {\n const dataPoints = slice\n .filter((histograms) => histograms[field].count > 0)\n .map(({ attributes, [field]: histogram }) => ({\n startTime,\n endTime,\n attributes,\n value: {\n count: histogram.count,\n sum: histogram.sum,\n min: histogram.min,\n max: histogram.max,\n scale: HISTOGRAM_SCALE,\n zeroCount: histogram.zeroCount,\n positive: { offset: histogram.offset, bucketCounts: histogram.bucketCounts },\n negative: { offset: 0, bucketCounts: [] },\n },\n }));\n if (dataPoints.length > 0) {\n metrics.push({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.DELTA,\n dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM,\n dataPoints,\n });\n }\n }\n await this.append(metrics);\n }\n }\n\n private async append(metrics: MetricData[]): Promise<void> {\n const payload = ProtobufMetricsSerializer.serializeRequest({\n resource: this.resource,\n scopeMetrics: [{ scope: { name: \"apitally\" }, metrics }],\n });\n if (payload) {\n await this.spool.append(\"metrics\", payload);\n }\n }\n\n private observeCpuUtilization(): number {\n const cpuUsage = process.cpuUsage();\n const nowMillis = performance.now();\n const cpuTimeMicros =\n cpuUsage.user - this.lastCpuUsage.user + (cpuUsage.system - this.lastCpuUsage.system);\n const elapsedMicros = (nowMillis - this.lastCpuUsageTimeMillis) * 1000;\n this.lastCpuUsage = cpuUsage;\n this.lastCpuUsageTimeMillis = nowMillis;\n return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;\n }\n}\n\n// Base-2 exponential histogram at a fixed scale for non-negative values, with the\n// bucket indexes of the OpenTelemetry specification.\nclass ExponentialHistogram {\n count = 0;\n sum = 0;\n min = Number.POSITIVE_INFINITY;\n max = Number.NEGATIVE_INFINITY;\n zeroCount = 0;\n offset = 0;\n bucketCounts: number[] = [];\n\n record(value: number): void {\n this.count++;\n this.sum += value;\n this.min = Math.min(this.min, value);\n this.max = Math.max(this.max, value);\n if (value === 0) {\n this.zeroCount++;\n return;\n }\n const index = mapToIndex(value);\n if (this.bucketCounts.length === 0) {\n this.offset = index;\n } else if (index < this.offset) {\n this.bucketCounts.unshift(...new Array<number>(this.offset - index).fill(0));\n this.offset = index;\n }\n const position = index - this.offset;\n while (this.bucketCounts.length <= position) {\n this.bucketCounts.push(0);\n }\n this.bucketCounts[position]++;\n }\n}\n\nfunction mapToIndex(value: number): number {\n const exponent = Math.floor(Math.log2(value));\n // Exact powers of two are the inclusive upper boundary of the bucket below.\n if (2 ** exponent === value) {\n return (exponent << HISTOGRAM_SCALE) - 1;\n }\n return Math.floor(Math.log(value) * SCALE_FACTOR);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { type Attributes, ValueType } from \"@opentelemetry/api\";\nimport { millisToHrTime } from \"@opentelemetry/core\";\nimport { ProtobufMetricsSerializer } from \"@opentelemetry/otlp-transformer\";\nimport type { Resource } from \"@opentelemetry/resources\";\nimport {\n AggregationTemporality,\n DataPointType,\n type ExponentialHistogramMetricData,\n type MetricData,\n} from \"@opentelemetry/sdk-metrics\";\nimport type { RequestRecord } from \"./context.js\";\nimport { logWarning } from \"./logger.js\";\nimport type { Spool } from \"./spool.js\";\n\nconst HISTOGRAM_SCALE = 3;\nconst SCALE_FACTOR = Math.LOG2E * 2 ** HISTOGRAM_SCALE;\n// Guards against misuse such as a request ID used as the consumer identifier.\nconst MAX_COMBINATIONS = 50_000;\n// Keeps each appended request well below the spool's 4 MB file rotation threshold.\nconst COMBINATIONS_PER_REQUEST = 1_000;\nconst CAPACITY_WARNING = `Apitally request metrics exceeded the capacity of ${MAX_COMBINATIONS.toLocaleString(\"en-US\")} distinct attribute combinations per collection interval, so some request metrics are missing.`;\nconst HISTOGRAM_METRICS = [\n [\"http.server.request.duration\", \"s\", \"duration\"],\n [\"http.server.request.body.size\", \"By\", \"requestBodySize\"],\n [\"http.server.response.body.size\", \"By\", \"responseBodySize\"],\n] as const;\n\ninterface RequestHistograms {\n attributes: Attributes;\n duration: ExponentialHistogram;\n requestBodySize: ExponentialHistogram;\n responseBodySize: ExponentialHistogram;\n}\n\n// Request histograms use finalized transport data, independent of span timing and sampling.\nexport class MetricsPipeline {\n private readonly resource: Resource;\n private readonly spool: Spool;\n private combinations = new Map<string, RequestHistograms>();\n private intervalStartTime = millisToHrTime(Date.now());\n private lastCpuUsage = process.cpuUsage();\n private lastCpuUsageTimeMillis = performance.now();\n\n constructor(resource: Resource, spool: Spool) {\n this.resource = resource;\n this.spool = spool;\n }\n\n // Excluded and sampled-out requests count; preflight, websocket, and unmatched routes do not.\n recordFromRequest(record: RequestRecord): void {\n if (record.dropReason === \"method\" || record.dropReason === \"scheme\") {\n return;\n }\n const source = record.attributes;\n const method = source[\"http.request.method\"] ?? source[\"http.method\"];\n const route = source[\"http.route\"];\n if (typeof route !== \"string\" || route === \"\") {\n return;\n }\n const attributes: Attributes = { \"http.route\": route };\n if (method !== undefined) {\n attributes[\"http.request.method\"] = method;\n }\n const statusCode = source[\"http.response.status_code\"] ?? source[\"http.status_code\"];\n if (statusCode !== undefined) {\n attributes[\"http.response.status_code\"] = statusCode;\n }\n const consumer = source[\"apitally.consumer.identifier\"];\n if (consumer !== undefined) {\n attributes[\"apitally.consumer.identifier\"] = consumer;\n }\n // Attributes are always set in the same order, so equal combinations give equal keys.\n const key = JSON.stringify(attributes);\n let histograms = this.combinations.get(key);\n if (histograms === undefined) {\n if (this.combinations.size >= MAX_COMBINATIONS) {\n logWarning(CAPACITY_WARNING);\n return;\n }\n histograms = {\n attributes,\n duration: new ExponentialHistogram(),\n requestBodySize: new ExponentialHistogram(),\n responseBodySize: new ExponentialHistogram(),\n };\n this.combinations.set(key, histograms);\n }\n if (typeof record.durationSeconds === \"number\") {\n histograms.duration.record(record.durationSeconds);\n }\n const requestBodySize = source[\"http.request.body.size\"];\n if (typeof requestBodySize === \"number\") {\n histograms.requestBodySize.record(requestBodySize);\n }\n const responseBodySize = source[\"http.response.body.size\"];\n if (typeof responseBodySize === \"number\") {\n histograms.responseBodySize.record(responseBodySize);\n }\n }\n\n async collectAndExport(): Promise<void> {\n const combinations = [...this.combinations.values()];\n this.combinations = new Map();\n const startTime = this.intervalStartTime;\n const endTime = millisToHrTime(Date.now());\n this.intervalStartTime = endTime;\n const gauges: [string, string, number][] = [\n [\"process.cpu.utilization\", \"1\", this.observeCpuUtilization()],\n [\"process.memory.usage\", \"By\", process.memoryUsage.rss()],\n [\"process.uptime\", \"s\", process.uptime()],\n ];\n // Apitally ingest treats every metrics export as a liveness signal, so the\n // gauges are appended on every collection.\n await this.append(\n gauges.map(([name, unit, value]) => ({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.CUMULATIVE,\n dataPointType: DataPointType.GAUGE,\n dataPoints: [{ startTime, endTime, attributes: {}, value }],\n })),\n );\n // Each combination's histograms stay in one request because Apitally ingest\n // joins them per resource entry.\n for (let index = 0; index < combinations.length; index += COMBINATIONS_PER_REQUEST) {\n const slice = combinations.slice(index, index + COMBINATIONS_PER_REQUEST);\n const metrics: ExponentialHistogramMetricData[] = [];\n for (const [name, unit, field] of HISTOGRAM_METRICS) {\n const dataPoints = slice\n .filter((histograms) => histograms[field].count > 0)\n .map(({ attributes, [field]: histogram }) => ({\n startTime,\n endTime,\n attributes,\n value: {\n count: histogram.count,\n sum: histogram.sum,\n min: histogram.min,\n max: histogram.max,\n scale: HISTOGRAM_SCALE,\n zeroCount: histogram.zeroCount,\n positive: { offset: histogram.offset, bucketCounts: histogram.bucketCounts },\n negative: { offset: 0, bucketCounts: [] },\n },\n }));\n if (dataPoints.length > 0) {\n metrics.push({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.DELTA,\n dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM,\n dataPoints,\n });\n }\n }\n await this.append(metrics);\n }\n }\n\n private async append(metrics: MetricData[]): Promise<void> {\n const payload = ProtobufMetricsSerializer.serializeRequest({\n resource: this.resource,\n scopeMetrics: [{ scope: { name: \"apitally\" }, metrics }],\n });\n if (payload) {\n await this.spool.append(\"metrics\", payload);\n }\n }\n\n private observeCpuUtilization(): number {\n const cpuUsage = process.cpuUsage();\n const nowMillis = performance.now();\n const cpuTimeMicros =\n cpuUsage.user - this.lastCpuUsage.user + (cpuUsage.system - this.lastCpuUsage.system);\n const elapsedMicros = (nowMillis - this.lastCpuUsageTimeMillis) * 1000;\n this.lastCpuUsage = cpuUsage;\n this.lastCpuUsageTimeMillis = nowMillis;\n return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;\n }\n}\n\n// Base-2 exponential histogram at a fixed scale for non-negative values, with the\n// bucket indexes of the OpenTelemetry specification.\nclass ExponentialHistogram {\n count = 0;\n sum = 0;\n min = Number.POSITIVE_INFINITY;\n max = Number.NEGATIVE_INFINITY;\n zeroCount = 0;\n offset = 0;\n bucketCounts: number[] = [];\n\n record(value: number): void {\n this.count++;\n this.sum += value;\n this.min = Math.min(this.min, value);\n this.max = Math.max(this.max, value);\n if (value === 0) {\n this.zeroCount++;\n return;\n }\n const index = mapToIndex(value);\n if (this.bucketCounts.length === 0) {\n this.offset = index;\n } else if (index < this.offset) {\n this.bucketCounts.unshift(...new Array<number>(this.offset - index).fill(0));\n this.offset = index;\n }\n const position = index - this.offset;\n while (this.bucketCounts.length <= position) {\n this.bucketCounts.push(0);\n }\n this.bucketCounts[position]++;\n }\n}\n\nfunction mapToIndex(value: number): number {\n const exponent = Math.floor(Math.log2(value));\n // Exact powers of two are the inclusive upper boundary of the bucket below.\n if (2 ** exponent === value) {\n return (exponent << HISTOGRAM_SCALE) - 1;\n }\n return Math.floor(Math.log(value) * SCALE_FACTOR);\n}\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_logger = require("./logger.cjs");
3
3
  const require_config = require("./config.cjs");
4
4
  const require_packageVersion = require("./packageVersion.cjs");
5
+ const require_spanProcessor = require("./spanProcessor.cjs");
5
6
  let _opentelemetry_api = require("@opentelemetry/api");
6
7
  let _opentelemetry_sdk_logs = require("@opentelemetry/sdk-logs");
7
8
  let _opentelemetry_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
@@ -14,9 +15,13 @@ let _opentelemetry_sdk_trace_node = require("@opentelemetry/sdk-trace-node");
14
15
  const MAX_ATTRIBUTE_VALUE_LENGTH = 65536;
15
16
  const DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name";
16
17
  var RequestRootedSampler = class {
17
- shouldSample(parentContext, _traceId, _spanName, spanKind) {
18
+ sampleRateBound;
19
+ constructor(sampleRateBound) {
20
+ this.sampleRateBound = sampleRateBound;
21
+ }
22
+ shouldSample(parentContext, traceId, _spanName, spanKind) {
18
23
  const parent = _opentelemetry_api.trace.getSpanContext(parentContext);
19
- return { decision: spanKind === _opentelemetry_api.SpanKind.SERVER || parent !== void 0 && !parent.isRemote && (parent.traceFlags & _opentelemetry_api.TraceFlags.SAMPLED) !== 0 ? _opentelemetry_sdk_trace_base.SamplingDecision.RECORD_AND_SAMPLED : _opentelemetry_sdk_trace_base.SamplingDecision.NOT_RECORD };
24
+ return { decision: (parent !== void 0 && (parent.traceFlags & _opentelemetry_api.TraceFlags.SAMPLED) !== 0 ? spanKind === _opentelemetry_api.SpanKind.SERVER || !parent.isRemote : spanKind === _opentelemetry_api.SpanKind.SERVER && require_spanProcessor.isTraceSampledIn(traceId, this.sampleRateBound)) ? _opentelemetry_sdk_trace_base.SamplingDecision.RECORD_AND_SAMPLED : _opentelemetry_sdk_trace_base.SamplingDecision.NOT_RECORD };
20
25
  }
21
26
  toString() {
22
27
  return "RequestRootedSampler";
@@ -36,8 +41,10 @@ function resolveEnvAndCreateResource() {
36
41
  };
37
42
  }
38
43
  function setupTracerProvider(resource, spanProcessors) {
44
+ const config = require_config.getConfig();
45
+ const sampleRate = config.sampleOnRequest ? 1 : config.sampleRate;
39
46
  const provider = new _opentelemetry_sdk_trace_node.NodeTracerProvider({
40
- sampler: new RequestRootedSampler(),
47
+ sampler: new RequestRootedSampler(require_spanProcessor.boundForSampleRate(sampleRate)),
41
48
  resource,
42
49
  spanProcessors,
43
50
  generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },
@@ -1 +1 @@
1
- {"version":3,"file":"providers.cjs","names":["trace","SpanKind","TraceFlags","SamplingDecision","detectResources","envDetector","getConfig","defaultResource","resourceFromAttributes","randomUUID","getDistroVersion","NodeTracerProvider","AsyncLocalStorageContextManager","context","W3CTraceContextPropagator","LoggerProvider","createContextKey"],"sources":["../src/providers.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n type Context,\n context,\n createContextKey,\n propagation,\n SpanKind,\n TraceFlags,\n trace,\n} from \"@opentelemetry/api\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport {\n defaultResource,\n detectResources,\n envDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { LoggerProvider, type LogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport {\n type Sampler,\n SamplingDecision,\n type SamplingResult,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport { getConfig } from \"./config.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\nimport { getDistroVersion } from \"./packageVersion.js\";\n\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 65_536;\nconst DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\";\n\nclass RequestRootedSampler implements Sampler {\n shouldSample(\n parentContext: Context,\n _traceId: string,\n _spanName: string,\n spanKind: SpanKind,\n ): SamplingResult {\n const parent = trace.getSpanContext(parentContext);\n const shouldRecord =\n spanKind === SpanKind.SERVER ||\n (parent !== undefined && !parent.isRemote && (parent.traceFlags & TraceFlags.SAMPLED) !== 0);\n return {\n decision: shouldRecord ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,\n };\n }\n\n toString(): string {\n return \"RequestRootedSampler\";\n }\n}\n\n// Apitally-Env and every signal's resource use the configured environment.\nexport function resolveEnvAndCreateResource(): { env: string; resource: Resource } {\n const environmentResource = detectResources({ detectors: [envDetector] });\n const env = getConfig().env;\n // defaultResource() ignores environment variables. Apitally attributes merge\n // last to keep the resource aligned with Apitally-Env.\n const resource = defaultResource()\n .merge(environmentResource)\n .merge(\n resourceFromAttributes({\n \"service.instance.id\": randomUUID(),\n [DEPLOYMENT_ENVIRONMENT_NAME]: env,\n \"telemetry.distro.name\": \"apitally-js\",\n \"telemetry.distro.version\": getDistroVersion(),\n }),\n );\n return { env, resource };\n}\n\nexport function setupTracerProvider(\n resource: Resource,\n spanProcessors: SpanProcessor[],\n): NodeTracerProvider | undefined {\n // Explicit sampler and length limits prevent OTel environment variables from\n // dropping upstream-unsampled SERVER spans or truncating long attributes.\n const provider = new NodeTracerProvider({\n sampler: new RequestRootedSampler(),\n resource,\n spanProcessors,\n generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n spanLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n });\n // OTel global setters reject duplicate or version-mismatched registrations,\n // preserving existing registrations.\n if (!trace.setGlobalTracerProvider(provider)) {\n return undefined;\n }\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n try {\n contextManager.disable();\n } catch (error) {\n logDebug(`Error disabling an unused OpenTelemetry context manager: ${String(error)}`);\n }\n warnIfContextDoesNotPropagate();\n }\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n return provider;\n}\n\n// The logger provider remains private because global registration could replace\n// or race a user's logs pipeline.\nexport function createLoggerProvider(\n resource: Resource,\n processors: LogRecordProcessor[],\n): LoggerProvider {\n return new LoggerProvider({ resource, processors });\n}\n\n// OTel exposes no context-manager getter, so a propagated probe verifies\n// registration. Without propagation, request contexts and suppression fail without an error.\nfunction warnIfContextDoesNotPropagate(): void {\n let isPropagated = false;\n try {\n const probeKey = createContextKey(\"apitally-context-probe\");\n isPropagated = context.with(\n context.active().setValue(probeKey, true),\n () => context.active().getValue(probeKey) === true,\n );\n } catch (error) {\n logDebug(`Error probing OpenTelemetry context propagation: ${String(error)}`);\n }\n if (!isPropagated) {\n logWarning(\n \"OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA+BA,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;AAEpC,IAAM,uBAAN,MAA8C;CAC5C,aACE,eACA,UACA,WACA,UACgB;EAChB,MAAM,SAASA,mBAAAA,MAAM,eAAe,aAAa;EAIjD,OAAO,EACL,UAHA,aAAaC,mBAAAA,SAAS,UACrB,WAAW,KAAA,KAAa,CAAC,OAAO,aAAa,OAAO,aAAaC,mBAAAA,WAAW,aAAa,IAEjEC,8BAAAA,iBAAiB,qBAAqBA,8BAAAA,iBAAiB,WAClF;CACF;CAEA,WAAmB;EACjB,OAAO;CACT;AACF;AAGA,SAAgB,8BAAmE;CACjF,MAAM,uBAAA,GAAsBC,yBAAAA,gBAAAA,CAAgB,EAAE,WAAW,CAACC,yBAAAA,WAAW,EAAE,CAAC;CACxE,MAAM,MAAMC,eAAAA,UAAU,CAAC,CAAC;CAaxB,OAAO;EAAE;EAAK,WAAA,GAVGC,yBAAAA,gBAAAA,CAAgB,CAAC,CAC/B,MAAM,mBAAmB,CAAC,CAC1B,OAAA,GACCC,yBAAAA,uBAAAA,CAAuB;GACrB,wBAAA,GAAuBC,YAAAA,WAAAA,CAAW;IACjC,8BAA8B;GAC/B,yBAAyB;GACzB,4BAA4BC,uBAAAA,iBAAiB;EAC/C,CAAC,CAEgB;CAAE;AACzB;AAEA,SAAgB,oBACd,UACA,gBACgC;CAGhC,MAAM,WAAW,IAAIC,8BAAAA,mBAAmB;EACtC,SAAS,IAAI,qBAAqB;EAClC;EACA;EACA,eAAe,EAAE,2BAA2B,2BAA2B;EACvE,YAAY,EAAE,2BAA2B,2BAA2B;CACtE,CAAC;CAGD,IAAI,CAACX,mBAAAA,MAAM,wBAAwB,QAAQ,GACzC;CAEF,MAAM,iBAAiB,IAAIY,mCAAAA,gCAAgC;CAC3D,eAAe,OAAO;CACtB,IAAI,CAACC,mBAAAA,QAAQ,wBAAwB,cAAc,GAAG;EACpD,IAAI;GACF,eAAe,QAAQ;EACzB,SAAS,OAAO;GACd,eAAA,SAAS,4DAA4D,OAAO,KAAK,GAAG;EACtF;EACA,8BAA8B;CAChC;CACA,mBAAA,YAAY,oBAAoB,IAAIC,oBAAAA,0BAA0B,CAAC;CAC/D,OAAO;AACT;AAIA,SAAgB,qBACd,UACA,YACgB;CAChB,OAAO,IAAIC,wBAAAA,eAAe;EAAE;EAAU;CAAW,CAAC;AACpD;AAIA,SAAS,gCAAsC;CAC7C,IAAI,eAAe;CACnB,IAAI;EACF,MAAM,YAAA,GAAWC,mBAAAA,iBAAAA,CAAiB,wBAAwB;EAC1D,eAAeH,mBAAAA,QAAQ,KACrBA,mBAAAA,QAAQ,OAAO,CAAC,CAAC,SAAS,UAAU,IAAI,SAClCA,mBAAAA,QAAQ,OAAO,CAAC,CAAC,SAAS,QAAQ,MAAM,IAChD;CACF,SAAS,OAAO;EACd,eAAA,SAAS,oDAAoD,OAAO,KAAK,GAAG;CAC9E;CACA,IAAI,CAAC,cACH,eAAA,WACE,wLACF;AAEJ"}
1
+ {"version":3,"file":"providers.cjs","names":["trace","TraceFlags","SpanKind","isTraceSampledIn","SamplingDecision","detectResources","envDetector","getConfig","defaultResource","resourceFromAttributes","randomUUID","getDistroVersion","NodeTracerProvider","boundForSampleRate","AsyncLocalStorageContextManager","context","W3CTraceContextPropagator","LoggerProvider","createContextKey"],"sources":["../src/providers.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n type Context,\n context,\n createContextKey,\n propagation,\n SpanKind,\n TraceFlags,\n trace,\n} from \"@opentelemetry/api\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport {\n defaultResource,\n detectResources,\n envDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { LoggerProvider, type LogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport {\n type Sampler,\n SamplingDecision,\n type SamplingResult,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport { getConfig } from \"./config.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\nimport { getDistroVersion } from \"./packageVersion.js\";\nimport { boundForSampleRate, isTraceSampledIn } from \"./spanProcessor.js\";\n\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 65_536;\nconst DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\";\n\n// Applies sampleRate to SERVER spans when they start, so sampled-out requests\n// record no spans. A sampled remote parent is followed for SERVER spans so upstream traces continue downstream.\nclass RequestRootedSampler implements Sampler {\n constructor(private readonly sampleRateBound: bigint) {}\n\n shouldSample(\n parentContext: Context,\n traceId: string,\n _spanName: string,\n spanKind: SpanKind,\n ): SamplingResult {\n const parent = trace.getSpanContext(parentContext);\n const isParentSampled = parent !== undefined && (parent.traceFlags & TraceFlags.SAMPLED) !== 0;\n const shouldRecord = isParentSampled\n ? spanKind === SpanKind.SERVER || !parent.isRemote\n : spanKind === SpanKind.SERVER && isTraceSampledIn(traceId, this.sampleRateBound);\n return {\n decision: shouldRecord ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,\n };\n }\n\n toString(): string {\n return \"RequestRootedSampler\";\n }\n}\n\n// Apitally-Env and every signal's resource use the configured environment.\nexport function resolveEnvAndCreateResource(): { env: string; resource: Resource } {\n const environmentResource = detectResources({ detectors: [envDetector] });\n const env = getConfig().env;\n // defaultResource() ignores environment variables. Apitally attributes merge\n // last to keep the resource aligned with Apitally-Env.\n const resource = defaultResource()\n .merge(environmentResource)\n .merge(\n resourceFromAttributes({\n \"service.instance.id\": randomUUID(),\n [DEPLOYMENT_ENVIRONMENT_NAME]: env,\n \"telemetry.distro.name\": \"apitally-js\",\n \"telemetry.distro.version\": getDistroVersion(),\n }),\n );\n return { env, resource };\n}\n\nexport function setupTracerProvider(\n resource: Resource,\n spanProcessors: SpanProcessor[],\n): NodeTracerProvider | undefined {\n const config = getConfig();\n // A sampleOnRequest callback can raise the rate, so it needs every request recorded.\n const sampleRate = config.sampleOnRequest ? 1 : config.sampleRate;\n // Explicit sampler and length limits prevent OTel environment variables from\n // changing which SERVER spans are recorded or truncating long attributes.\n const provider = new NodeTracerProvider({\n sampler: new RequestRootedSampler(boundForSampleRate(sampleRate)),\n resource,\n spanProcessors,\n generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n spanLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n });\n // OTel global setters reject duplicate or version-mismatched registrations,\n // preserving existing registrations.\n if (!trace.setGlobalTracerProvider(provider)) {\n return undefined;\n }\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n try {\n contextManager.disable();\n } catch (error) {\n logDebug(`Error disabling an unused OpenTelemetry context manager: ${String(error)}`);\n }\n warnIfContextDoesNotPropagate();\n }\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n return provider;\n}\n\n// The logger provider remains private because global registration could replace\n// or race a user's logs pipeline.\nexport function createLoggerProvider(\n resource: Resource,\n processors: LogRecordProcessor[],\n): LoggerProvider {\n return new LoggerProvider({ resource, processors });\n}\n\n// OTel exposes no context-manager getter, so a propagated probe verifies\n// registration. Without propagation, request contexts and suppression fail without an error.\nfunction warnIfContextDoesNotPropagate(): void {\n let isPropagated = false;\n try {\n const probeKey = createContextKey(\"apitally-context-probe\");\n isPropagated = context.with(\n context.active().setValue(probeKey, true),\n () => context.active().getValue(probeKey) === true,\n );\n } catch (error) {\n logDebug(`Error probing OpenTelemetry context propagation: ${String(error)}`);\n }\n if (!isPropagated) {\n logWarning(\n \"OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAgCA,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;AAIpC,IAAM,uBAAN,MAA8C;CACf;CAA7B,YAAY,iBAA0C;EAAzB,KAAA,kBAAA;CAA0B;CAEvD,aACE,eACA,SACA,WACA,UACgB;EAChB,MAAM,SAASA,mBAAAA,MAAM,eAAe,aAAa;EAKjD,OAAO,EACL,WALsB,WAAW,KAAA,MAAc,OAAO,aAAaC,mBAAAA,WAAW,aAAa,IAEzF,aAAaC,mBAAAA,SAAS,UAAU,CAAC,OAAO,WACxC,aAAaA,mBAAAA,SAAS,UAAUC,sBAAAA,iBAAiB,SAAS,KAAK,eAAe,KAEvDC,8BAAAA,iBAAiB,qBAAqBA,8BAAAA,iBAAiB,WAClF;CACF;CAEA,WAAmB;EACjB,OAAO;CACT;AACF;AAGA,SAAgB,8BAAmE;CACjF,MAAM,uBAAA,GAAsBC,yBAAAA,gBAAAA,CAAgB,EAAE,WAAW,CAACC,yBAAAA,WAAW,EAAE,CAAC;CACxE,MAAM,MAAMC,eAAAA,UAAU,CAAC,CAAC;CAaxB,OAAO;EAAE;EAAK,WAAA,GAVGC,yBAAAA,gBAAAA,CAAgB,CAAC,CAC/B,MAAM,mBAAmB,CAAC,CAC1B,OAAA,GACCC,yBAAAA,uBAAAA,CAAuB;GACrB,wBAAA,GAAuBC,YAAAA,WAAAA,CAAW;IACjC,8BAA8B;GAC/B,yBAAyB;GACzB,4BAA4BC,uBAAAA,iBAAiB;EAC/C,CAAC,CAEgB;CAAE;AACzB;AAEA,SAAgB,oBACd,UACA,gBACgC;CAChC,MAAM,SAASJ,eAAAA,UAAU;CAEzB,MAAM,aAAa,OAAO,kBAAkB,IAAI,OAAO;CAGvD,MAAM,WAAW,IAAIK,8BAAAA,mBAAmB;EACtC,SAAS,IAAI,qBAAqBC,sBAAAA,mBAAmB,UAAU,CAAC;EAChE;EACA;EACA,eAAe,EAAE,2BAA2B,2BAA2B;EACvE,YAAY,EAAE,2BAA2B,2BAA2B;CACtE,CAAC;CAGD,IAAI,CAACb,mBAAAA,MAAM,wBAAwB,QAAQ,GACzC;CAEF,MAAM,iBAAiB,IAAIc,mCAAAA,gCAAgC;CAC3D,eAAe,OAAO;CACtB,IAAI,CAACC,mBAAAA,QAAQ,wBAAwB,cAAc,GAAG;EACpD,IAAI;GACF,eAAe,QAAQ;EACzB,SAAS,OAAO;GACd,eAAA,SAAS,4DAA4D,OAAO,KAAK,GAAG;EACtF;EACA,8BAA8B;CAChC;CACA,mBAAA,YAAY,oBAAoB,IAAIC,oBAAAA,0BAA0B,CAAC;CAC/D,OAAO;AACT;AAIA,SAAgB,qBACd,UACA,YACgB;CAChB,OAAO,IAAIC,wBAAAA,eAAe;EAAE;EAAU;CAAW,CAAC;AACpD;AAIA,SAAS,gCAAsC;CAC7C,IAAI,eAAe;CACnB,IAAI;EACF,MAAM,YAAA,GAAWC,mBAAAA,iBAAAA,CAAiB,wBAAwB;EAC1D,eAAeH,mBAAAA,QAAQ,KACrBA,mBAAAA,QAAQ,OAAO,CAAC,CAAC,SAAS,UAAU,IAAI,SAClCA,mBAAAA,QAAQ,OAAO,CAAC,CAAC,SAAS,QAAQ,MAAM,IAChD;CACF,SAAS,OAAO;EACd,eAAA,SAAS,oDAAoD,OAAO,KAAK,GAAG;CAC9E;CACA,IAAI,CAAC,cACH,eAAA,WACE,wLACF;AAEJ"}
package/dist/providers.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { logDebug, logWarning } from "./logger.js";
2
2
  import { getConfig } from "./config.js";
3
3
  import { getDistroVersion } from "./packageVersion.js";
4
+ import { boundForSampleRate, isTraceSampledIn } from "./spanProcessor.js";
4
5
  import { SpanKind, TraceFlags, context, createContextKey, propagation, trace } from "@opentelemetry/api";
5
6
  import { LoggerProvider } from "@opentelemetry/sdk-logs";
6
7
  import { SamplingDecision } from "@opentelemetry/sdk-trace-base";
@@ -13,9 +14,13 @@ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
13
14
  const MAX_ATTRIBUTE_VALUE_LENGTH = 65536;
14
15
  const DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name";
15
16
  var RequestRootedSampler = class {
16
- shouldSample(parentContext, _traceId, _spanName, spanKind) {
17
+ sampleRateBound;
18
+ constructor(sampleRateBound) {
19
+ this.sampleRateBound = sampleRateBound;
20
+ }
21
+ shouldSample(parentContext, traceId, _spanName, spanKind) {
17
22
  const parent = trace.getSpanContext(parentContext);
18
- return { decision: spanKind === SpanKind.SERVER || parent !== void 0 && !parent.isRemote && (parent.traceFlags & TraceFlags.SAMPLED) !== 0 ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD };
23
+ return { decision: (parent !== void 0 && (parent.traceFlags & TraceFlags.SAMPLED) !== 0 ? spanKind === SpanKind.SERVER || !parent.isRemote : spanKind === SpanKind.SERVER && isTraceSampledIn(traceId, this.sampleRateBound)) ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD };
19
24
  }
20
25
  toString() {
21
26
  return "RequestRootedSampler";
@@ -35,8 +40,10 @@ function resolveEnvAndCreateResource() {
35
40
  };
36
41
  }
37
42
  function setupTracerProvider(resource, spanProcessors) {
43
+ const config = getConfig();
44
+ const sampleRate = config.sampleOnRequest ? 1 : config.sampleRate;
38
45
  const provider = new NodeTracerProvider({
39
- sampler: new RequestRootedSampler(),
46
+ sampler: new RequestRootedSampler(boundForSampleRate(sampleRate)),
40
47
  resource,
41
48
  spanProcessors,
42
49
  generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },
@@ -1 +1 @@
1
- {"version":3,"file":"providers.js","names":[],"sources":["../src/providers.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n type Context,\n context,\n createContextKey,\n propagation,\n SpanKind,\n TraceFlags,\n trace,\n} from \"@opentelemetry/api\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport {\n defaultResource,\n detectResources,\n envDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { LoggerProvider, type LogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport {\n type Sampler,\n SamplingDecision,\n type SamplingResult,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport { getConfig } from \"./config.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\nimport { getDistroVersion } from \"./packageVersion.js\";\n\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 65_536;\nconst DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\";\n\nclass RequestRootedSampler implements Sampler {\n shouldSample(\n parentContext: Context,\n _traceId: string,\n _spanName: string,\n spanKind: SpanKind,\n ): SamplingResult {\n const parent = trace.getSpanContext(parentContext);\n const shouldRecord =\n spanKind === SpanKind.SERVER ||\n (parent !== undefined && !parent.isRemote && (parent.traceFlags & TraceFlags.SAMPLED) !== 0);\n return {\n decision: shouldRecord ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,\n };\n }\n\n toString(): string {\n return \"RequestRootedSampler\";\n }\n}\n\n// Apitally-Env and every signal's resource use the configured environment.\nexport function resolveEnvAndCreateResource(): { env: string; resource: Resource } {\n const environmentResource = detectResources({ detectors: [envDetector] });\n const env = getConfig().env;\n // defaultResource() ignores environment variables. Apitally attributes merge\n // last to keep the resource aligned with Apitally-Env.\n const resource = defaultResource()\n .merge(environmentResource)\n .merge(\n resourceFromAttributes({\n \"service.instance.id\": randomUUID(),\n [DEPLOYMENT_ENVIRONMENT_NAME]: env,\n \"telemetry.distro.name\": \"apitally-js\",\n \"telemetry.distro.version\": getDistroVersion(),\n }),\n );\n return { env, resource };\n}\n\nexport function setupTracerProvider(\n resource: Resource,\n spanProcessors: SpanProcessor[],\n): NodeTracerProvider | undefined {\n // Explicit sampler and length limits prevent OTel environment variables from\n // dropping upstream-unsampled SERVER spans or truncating long attributes.\n const provider = new NodeTracerProvider({\n sampler: new RequestRootedSampler(),\n resource,\n spanProcessors,\n generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n spanLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n });\n // OTel global setters reject duplicate or version-mismatched registrations,\n // preserving existing registrations.\n if (!trace.setGlobalTracerProvider(provider)) {\n return undefined;\n }\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n try {\n contextManager.disable();\n } catch (error) {\n logDebug(`Error disabling an unused OpenTelemetry context manager: ${String(error)}`);\n }\n warnIfContextDoesNotPropagate();\n }\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n return provider;\n}\n\n// The logger provider remains private because global registration could replace\n// or race a user's logs pipeline.\nexport function createLoggerProvider(\n resource: Resource,\n processors: LogRecordProcessor[],\n): LoggerProvider {\n return new LoggerProvider({ resource, processors });\n}\n\n// OTel exposes no context-manager getter, so a propagated probe verifies\n// registration. Without propagation, request contexts and suppression fail without an error.\nfunction warnIfContextDoesNotPropagate(): void {\n let isPropagated = false;\n try {\n const probeKey = createContextKey(\"apitally-context-probe\");\n isPropagated = context.with(\n context.active().setValue(probeKey, true),\n () => context.active().getValue(probeKey) === true,\n );\n } catch (error) {\n logDebug(`Error probing OpenTelemetry context propagation: ${String(error)}`);\n }\n if (!isPropagated) {\n logWarning(\n \"OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;AA+BA,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;AAEpC,IAAM,uBAAN,MAA8C;CAC5C,aACE,eACA,UACA,WACA,UACgB;EAChB,MAAM,SAAS,MAAM,eAAe,aAAa;EAIjD,OAAO,EACL,UAHA,aAAa,SAAS,UACrB,WAAW,KAAA,KAAa,CAAC,OAAO,aAAa,OAAO,aAAa,WAAW,aAAa,IAEjE,iBAAiB,qBAAqB,iBAAiB,WAClF;CACF;CAEA,WAAmB;EACjB,OAAO;CACT;AACF;AAGA,SAAgB,8BAAmE;CACjF,MAAM,sBAAsB,gBAAgB,EAAE,WAAW,CAAC,WAAW,EAAE,CAAC;CACxE,MAAM,MAAM,UAAU,CAAC,CAAC;CAaxB,OAAO;EAAE;EAAK,UAVG,gBAAgB,CAAC,CAC/B,MAAM,mBAAmB,CAAC,CAC1B,MACC,uBAAuB;GACrB,uBAAuB,WAAW;IACjC,8BAA8B;GAC/B,yBAAyB;GACzB,4BAA4B,iBAAiB;EAC/C,CAAC,CAEgB;CAAE;AACzB;AAEA,SAAgB,oBACd,UACA,gBACgC;CAGhC,MAAM,WAAW,IAAI,mBAAmB;EACtC,SAAS,IAAI,qBAAqB;EAClC;EACA;EACA,eAAe,EAAE,2BAA2B,2BAA2B;EACvE,YAAY,EAAE,2BAA2B,2BAA2B;CACtE,CAAC;CAGD,IAAI,CAAC,MAAM,wBAAwB,QAAQ,GACzC;CAEF,MAAM,iBAAiB,IAAI,gCAAgC;CAC3D,eAAe,OAAO;CACtB,IAAI,CAAC,QAAQ,wBAAwB,cAAc,GAAG;EACpD,IAAI;GACF,eAAe,QAAQ;EACzB,SAAS,OAAO;GACd,SAAS,4DAA4D,OAAO,KAAK,GAAG;EACtF;EACA,8BAA8B;CAChC;CACA,YAAY,oBAAoB,IAAI,0BAA0B,CAAC;CAC/D,OAAO;AACT;AAIA,SAAgB,qBACd,UACA,YACgB;CAChB,OAAO,IAAI,eAAe;EAAE;EAAU;CAAW,CAAC;AACpD;AAIA,SAAS,gCAAsC;CAC7C,IAAI,eAAe;CACnB,IAAI;EACF,MAAM,WAAW,iBAAiB,wBAAwB;EAC1D,eAAe,QAAQ,KACrB,QAAQ,OAAO,CAAC,CAAC,SAAS,UAAU,IAAI,SAClC,QAAQ,OAAO,CAAC,CAAC,SAAS,QAAQ,MAAM,IAChD;CACF,SAAS,OAAO;EACd,SAAS,oDAAoD,OAAO,KAAK,GAAG;CAC9E;CACA,IAAI,CAAC,cACH,WACE,wLACF;AAEJ"}
1
+ {"version":3,"file":"providers.js","names":[],"sources":["../src/providers.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n type Context,\n context,\n createContextKey,\n propagation,\n SpanKind,\n TraceFlags,\n trace,\n} from \"@opentelemetry/api\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport {\n defaultResource,\n detectResources,\n envDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { LoggerProvider, type LogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport {\n type Sampler,\n SamplingDecision,\n type SamplingResult,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport { getConfig } from \"./config.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\nimport { getDistroVersion } from \"./packageVersion.js\";\nimport { boundForSampleRate, isTraceSampledIn } from \"./spanProcessor.js\";\n\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 65_536;\nconst DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\";\n\n// Applies sampleRate to SERVER spans when they start, so sampled-out requests\n// record no spans. A sampled remote parent is followed for SERVER spans so upstream traces continue downstream.\nclass RequestRootedSampler implements Sampler {\n constructor(private readonly sampleRateBound: bigint) {}\n\n shouldSample(\n parentContext: Context,\n traceId: string,\n _spanName: string,\n spanKind: SpanKind,\n ): SamplingResult {\n const parent = trace.getSpanContext(parentContext);\n const isParentSampled = parent !== undefined && (parent.traceFlags & TraceFlags.SAMPLED) !== 0;\n const shouldRecord = isParentSampled\n ? spanKind === SpanKind.SERVER || !parent.isRemote\n : spanKind === SpanKind.SERVER && isTraceSampledIn(traceId, this.sampleRateBound);\n return {\n decision: shouldRecord ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,\n };\n }\n\n toString(): string {\n return \"RequestRootedSampler\";\n }\n}\n\n// Apitally-Env and every signal's resource use the configured environment.\nexport function resolveEnvAndCreateResource(): { env: string; resource: Resource } {\n const environmentResource = detectResources({ detectors: [envDetector] });\n const env = getConfig().env;\n // defaultResource() ignores environment variables. Apitally attributes merge\n // last to keep the resource aligned with Apitally-Env.\n const resource = defaultResource()\n .merge(environmentResource)\n .merge(\n resourceFromAttributes({\n \"service.instance.id\": randomUUID(),\n [DEPLOYMENT_ENVIRONMENT_NAME]: env,\n \"telemetry.distro.name\": \"apitally-js\",\n \"telemetry.distro.version\": getDistroVersion(),\n }),\n );\n return { env, resource };\n}\n\nexport function setupTracerProvider(\n resource: Resource,\n spanProcessors: SpanProcessor[],\n): NodeTracerProvider | undefined {\n const config = getConfig();\n // A sampleOnRequest callback can raise the rate, so it needs every request recorded.\n const sampleRate = config.sampleOnRequest ? 1 : config.sampleRate;\n // Explicit sampler and length limits prevent OTel environment variables from\n // changing which SERVER spans are recorded or truncating long attributes.\n const provider = new NodeTracerProvider({\n sampler: new RequestRootedSampler(boundForSampleRate(sampleRate)),\n resource,\n spanProcessors,\n generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n spanLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n });\n // OTel global setters reject duplicate or version-mismatched registrations,\n // preserving existing registrations.\n if (!trace.setGlobalTracerProvider(provider)) {\n return undefined;\n }\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n try {\n contextManager.disable();\n } catch (error) {\n logDebug(`Error disabling an unused OpenTelemetry context manager: ${String(error)}`);\n }\n warnIfContextDoesNotPropagate();\n }\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n return provider;\n}\n\n// The logger provider remains private because global registration could replace\n// or race a user's logs pipeline.\nexport function createLoggerProvider(\n resource: Resource,\n processors: LogRecordProcessor[],\n): LoggerProvider {\n return new LoggerProvider({ resource, processors });\n}\n\n// OTel exposes no context-manager getter, so a propagated probe verifies\n// registration. Without propagation, request contexts and suppression fail without an error.\nfunction warnIfContextDoesNotPropagate(): void {\n let isPropagated = false;\n try {\n const probeKey = createContextKey(\"apitally-context-probe\");\n isPropagated = context.with(\n context.active().setValue(probeKey, true),\n () => context.active().getValue(probeKey) === true,\n );\n } catch (error) {\n logDebug(`Error probing OpenTelemetry context propagation: ${String(error)}`);\n }\n if (!isPropagated) {\n logWarning(\n \"OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgCA,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;AAIpC,IAAM,uBAAN,MAA8C;CACf;CAA7B,YAAY,iBAA0C;EAAzB,KAAA,kBAAA;CAA0B;CAEvD,aACE,eACA,SACA,WACA,UACgB;EAChB,MAAM,SAAS,MAAM,eAAe,aAAa;EAKjD,OAAO,EACL,WALsB,WAAW,KAAA,MAAc,OAAO,aAAa,WAAW,aAAa,IAEzF,aAAa,SAAS,UAAU,CAAC,OAAO,WACxC,aAAa,SAAS,UAAU,iBAAiB,SAAS,KAAK,eAAe,KAEvD,iBAAiB,qBAAqB,iBAAiB,WAClF;CACF;CAEA,WAAmB;EACjB,OAAO;CACT;AACF;AAGA,SAAgB,8BAAmE;CACjF,MAAM,sBAAsB,gBAAgB,EAAE,WAAW,CAAC,WAAW,EAAE,CAAC;CACxE,MAAM,MAAM,UAAU,CAAC,CAAC;CAaxB,OAAO;EAAE;EAAK,UAVG,gBAAgB,CAAC,CAC/B,MAAM,mBAAmB,CAAC,CAC1B,MACC,uBAAuB;GACrB,uBAAuB,WAAW;IACjC,8BAA8B;GAC/B,yBAAyB;GACzB,4BAA4B,iBAAiB;EAC/C,CAAC,CAEgB;CAAE;AACzB;AAEA,SAAgB,oBACd,UACA,gBACgC;CAChC,MAAM,SAAS,UAAU;CAEzB,MAAM,aAAa,OAAO,kBAAkB,IAAI,OAAO;CAGvD,MAAM,WAAW,IAAI,mBAAmB;EACtC,SAAS,IAAI,qBAAqB,mBAAmB,UAAU,CAAC;EAChE;EACA;EACA,eAAe,EAAE,2BAA2B,2BAA2B;EACvE,YAAY,EAAE,2BAA2B,2BAA2B;CACtE,CAAC;CAGD,IAAI,CAAC,MAAM,wBAAwB,QAAQ,GACzC;CAEF,MAAM,iBAAiB,IAAI,gCAAgC;CAC3D,eAAe,OAAO;CACtB,IAAI,CAAC,QAAQ,wBAAwB,cAAc,GAAG;EACpD,IAAI;GACF,eAAe,QAAQ;EACzB,SAAS,OAAO;GACd,SAAS,4DAA4D,OAAO,KAAK,GAAG;EACtF;EACA,8BAA8B;CAChC;CACA,YAAY,oBAAoB,IAAI,0BAA0B,CAAC;CAC/D,OAAO;AACT;AAIA,SAAgB,qBACd,UACA,YACgB;CAChB,OAAO,IAAI,eAAe;EAAE;EAAU;CAAW,CAAC;AACpD;AAIA,SAAS,gCAAsC;CAC7C,IAAI,eAAe;CACnB,IAAI;EACF,MAAM,WAAW,iBAAiB,wBAAwB;EAC1D,eAAe,QAAQ,KACrB,QAAQ,OAAO,CAAC,CAAC,SAAS,UAAU,IAAI,SAClC,QAAQ,OAAO,CAAC,CAAC,SAAS,QAAQ,MAAM,IAChD;CACF,SAAS,OAAO;EACd,SAAS,oDAAoD,OAAO,KAAK,GAAG;CAC9E;CACA,IAAI,CAAC,cACH,WACE,wLACF;AAEJ"}
@@ -43,10 +43,11 @@ var Redaction = class {
43
43
  } catch {
44
44
  return text;
45
45
  }
46
- return JSON.stringify(this.redactBodyFields(data));
46
+ this.redactBodyFields(data);
47
+ return JSON.stringify(data);
47
48
  }
48
49
  shouldRedactHeader(name) {
49
- return require_config.matchesAny(this.headerPatterns, name) || require_config.matchesAny(this.headerPatterns, name.replaceAll("_", "-"));
50
+ return require_config.matchesAny(this.headerPatterns, name) || name.includes("_") && require_config.matchesAny(this.headerPatterns, name.replaceAll("_", "-"));
50
51
  }
51
52
  shouldRedactQueryParam(name) {
52
53
  return require_config.matchesAny(this.queryParamPatterns, name);
@@ -55,9 +56,12 @@ var Redaction = class {
55
56
  return require_config.matchesAny(this.bodyFieldPatterns, name);
56
57
  }
57
58
  redactBodyFields(data) {
58
- if (Array.isArray(data)) return data.map((item) => this.redactBodyFields(item));
59
- if (typeof data === "object" && data !== null) return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, typeof value === "string" && this.shouldRedactBodyField(key) ? REDACTED : this.redactBodyFields(value)]));
60
- return data;
59
+ if (Array.isArray(data)) for (const item of data) this.redactBodyFields(item);
60
+ else if (typeof data === "object" && data !== null) for (const key of Object.keys(data)) {
61
+ const value = data[key];
62
+ if (typeof value === "string" && this.shouldRedactBodyField(key)) data[key] = REDACTED;
63
+ else this.redactBodyFields(value);
64
+ }
61
65
  }
62
66
  };
63
67
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"redaction.cjs","names":["getConfig","compilePatterns","DEFAULT_MASK_QUERY_PARAMS","DEFAULT_MASK_HEADERS","DEFAULT_MASK_BODY_FIELDS","matchesAny"],"sources":["../src/redaction.ts"],"sourcesContent":["import {\n compilePatterns,\n DEFAULT_MASK_BODY_FIELDS,\n DEFAULT_MASK_HEADERS,\n DEFAULT_MASK_QUERY_PARAMS,\n getConfig,\n matchesAny,\n} from \"./config.js\";\n\nexport const REDACTED = \"[REDACTED]\";\n\n// Location headers can contain secret query values, so redaction targets only\n// their query parameters.\nconst URL_HEADER_NAMES = new Set([\"location\", \"content-location\"]);\n\nconst utf8Decoder = new TextDecoder(\"utf-8\", { fatal: true });\n\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n\nexport class Redaction {\n private queryParamPatterns: RegExp[];\n private headerPatterns: RegExp[];\n private bodyFieldPatterns: RegExp[];\n\n constructor() {\n const config = getConfig();\n this.queryParamPatterns = compilePatterns(DEFAULT_MASK_QUERY_PARAMS, config.maskQueryParams);\n this.headerPatterns = compilePatterns(DEFAULT_MASK_HEADERS, config.maskHeaders);\n this.bodyFieldPatterns = compilePatterns(DEFAULT_MASK_BODY_FIELDS, config.maskBodyFields);\n }\n\n // Redacts matching parameter names in a path with a query, a full URL, or a\n // bare query string when `assumeQuery` is true.\n redactQueryParams(value: string, assumeQuery = true): string {\n const separatorIndex = assumeQuery ? -1 : value.indexOf(\"?\");\n if (separatorIndex === -1 && !assumeQuery) {\n return value;\n }\n const base = separatorIndex === -1 ? \"\" : value.slice(0, separatorIndex);\n const query = separatorIndex === -1 ? value : value.slice(separatorIndex + 1);\n const redactedParams = new URLSearchParams();\n for (const [name, paramValue] of new URLSearchParams(query)) {\n redactedParams.append(name, this.shouldRedactQueryParam(name) ? REDACTED : paramValue);\n }\n return separatorIndex === -1\n ? redactedParams.toString()\n : `${base}?${redactedParams.toString()}`;\n }\n\n redactHeaderValue(name: string, value: string | string[]): string | string[];\n redactHeaderValue(name: string, value: unknown): unknown;\n redactHeaderValue(name: string, value: unknown): unknown {\n if (this.shouldRedactHeader(name)) {\n return Array.isArray(value) ? [REDACTED] : REDACTED;\n }\n if (!URL_HEADER_NAMES.has(name.toLowerCase())) {\n return value;\n }\n if (typeof value === \"string\") {\n return this.redactQueryParams(value, false);\n }\n if (Array.isArray(value)) {\n return value.map((item) =>\n typeof item === \"string\" ? this.redactQueryParams(item, false) : item,\n );\n }\n return value;\n }\n\n // Whether a body is JSON is decided by a parse attempt, never by content type.\n redactBody(body: Buffer): string | Buffer {\n let text: string;\n try {\n text = utf8Decoder.decode(body);\n } catch {\n return body;\n }\n let data: JsonValue;\n try {\n data = JSON.parse(text);\n } catch {\n return text;\n }\n return JSON.stringify(this.redactBodyFields(data));\n }\n\n private shouldRedactHeader(name: string): boolean {\n // OpenTelemetry HTTP header attributes normalize hyphens to underscores.\n return (\n matchesAny(this.headerPatterns, name) ||\n matchesAny(this.headerPatterns, name.replaceAll(\"_\", \"-\"))\n );\n }\n\n private shouldRedactQueryParam(name: string): boolean {\n return matchesAny(this.queryParamPatterns, name);\n }\n\n private shouldRedactBodyField(name: string): boolean {\n return matchesAny(this.bodyFieldPatterns, name);\n }\n\n private redactBodyFields(data: JsonValue): JsonValue {\n if (Array.isArray(data)) {\n return data.map((item) => this.redactBodyFields(item));\n }\n if (typeof data === \"object\" && data !== null) {\n return Object.fromEntries(\n Object.entries(data).map(([key, value]) => [\n key,\n typeof value === \"string\" && this.shouldRedactBodyField(key)\n ? REDACTED\n : this.redactBodyFields(value),\n ]),\n );\n }\n return data;\n }\n}\n"],"mappings":";;;AASA,MAAa,WAAW;AAIxB,MAAM,mCAAmB,IAAI,IAAI,CAAC,YAAY,kBAAkB,CAAC;AAEjE,MAAM,cAAc,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC;AAI5D,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CAEA,cAAc;EACZ,MAAM,SAASA,eAAAA,UAAU;EACzB,KAAK,qBAAqBC,eAAAA,gBAAgBC,eAAAA,2BAA2B,OAAO,eAAe;EAC3F,KAAK,iBAAiBD,eAAAA,gBAAgBE,eAAAA,sBAAsB,OAAO,WAAW;EAC9E,KAAK,oBAAoBF,eAAAA,gBAAgBG,eAAAA,0BAA0B,OAAO,cAAc;CAC1F;CAIA,kBAAkB,OAAe,cAAc,MAAc;EAC3D,MAAM,iBAAiB,cAAc,KAAK,MAAM,QAAQ,GAAG;EAC3D,IAAI,mBAAmB,MAAM,CAAC,aAC5B,OAAO;EAET,MAAM,OAAO,mBAAmB,KAAK,KAAK,MAAM,MAAM,GAAG,cAAc;EACvE,MAAM,QAAQ,mBAAmB,KAAK,QAAQ,MAAM,MAAM,iBAAiB,CAAC;EAC5E,MAAM,iBAAiB,IAAI,gBAAgB;EAC3C,KAAK,MAAM,CAAC,MAAM,eAAe,IAAI,gBAAgB,KAAK,GACxD,eAAe,OAAO,MAAM,KAAK,uBAAuB,IAAI,IAAI,WAAW,UAAU;EAEvF,OAAO,mBAAmB,KACtB,eAAe,SAAS,IACxB,GAAG,KAAK,GAAG,eAAe,SAAS;CACzC;CAIA,kBAAkB,MAAc,OAAyB;EACvD,IAAI,KAAK,mBAAmB,IAAI,GAC9B,OAAO,MAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI;EAE7C,IAAI,CAAC,iBAAiB,IAAI,KAAK,YAAY,CAAC,GAC1C,OAAO;EAET,IAAI,OAAO,UAAU,UACnB,OAAO,KAAK,kBAAkB,OAAO,KAAK;EAE5C,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAChB,OAAO,SAAS,WAAW,KAAK,kBAAkB,MAAM,KAAK,IAAI,IACnE;EAEF,OAAO;CACT;CAGA,WAAW,MAA+B;EACxC,IAAI;EACJ,IAAI;GACF,OAAO,YAAY,OAAO,IAAI;EAChC,QAAQ;GACN,OAAO;EACT;EACA,IAAI;EACJ,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ;GACN,OAAO;EACT;EACA,OAAO,KAAK,UAAU,KAAK,iBAAiB,IAAI,CAAC;CACnD;CAEA,mBAA2B,MAAuB;EAEhD,OACEC,eAAAA,WAAW,KAAK,gBAAgB,IAAI,KACpCA,eAAAA,WAAW,KAAK,gBAAgB,KAAK,WAAW,KAAK,GAAG,CAAC;CAE7D;CAEA,uBAA+B,MAAuB;EACpD,OAAOA,eAAAA,WAAW,KAAK,oBAAoB,IAAI;CACjD;CAEA,sBAA8B,MAAuB;EACnD,OAAOA,eAAAA,WAAW,KAAK,mBAAmB,IAAI;CAChD;CAEA,iBAAyB,MAA4B;EACnD,IAAI,MAAM,QAAQ,IAAI,GACpB,OAAO,KAAK,KAAK,SAAS,KAAK,iBAAiB,IAAI,CAAC;EAEvD,IAAI,OAAO,SAAS,YAAY,SAAS,MACvC,OAAO,OAAO,YACZ,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACzC,KACA,OAAO,UAAU,YAAY,KAAK,sBAAsB,GAAG,IACvD,WACA,KAAK,iBAAiB,KAAK,CACjC,CAAC,CACH;EAEF,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"redaction.cjs","names":["getConfig","compilePatterns","DEFAULT_MASK_QUERY_PARAMS","DEFAULT_MASK_HEADERS","DEFAULT_MASK_BODY_FIELDS","matchesAny"],"sources":["../src/redaction.ts"],"sourcesContent":["import {\n compilePatterns,\n DEFAULT_MASK_BODY_FIELDS,\n DEFAULT_MASK_HEADERS,\n DEFAULT_MASK_QUERY_PARAMS,\n getConfig,\n matchesAny,\n} from \"./config.js\";\n\nexport const REDACTED = \"[REDACTED]\";\n\n// Location headers can contain secret query values, so redaction targets only\n// their query parameters.\nconst URL_HEADER_NAMES = new Set([\"location\", \"content-location\"]);\n\nconst utf8Decoder = new TextDecoder(\"utf-8\", { fatal: true });\n\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n\nexport class Redaction {\n private queryParamPatterns: RegExp[];\n private headerPatterns: RegExp[];\n private bodyFieldPatterns: RegExp[];\n\n constructor() {\n const config = getConfig();\n this.queryParamPatterns = compilePatterns(DEFAULT_MASK_QUERY_PARAMS, config.maskQueryParams);\n this.headerPatterns = compilePatterns(DEFAULT_MASK_HEADERS, config.maskHeaders);\n this.bodyFieldPatterns = compilePatterns(DEFAULT_MASK_BODY_FIELDS, config.maskBodyFields);\n }\n\n // Redacts matching parameter names in a path with a query, a full URL, or a\n // bare query string when `assumeQuery` is true.\n redactQueryParams(value: string, assumeQuery = true): string {\n const separatorIndex = assumeQuery ? -1 : value.indexOf(\"?\");\n if (separatorIndex === -1 && !assumeQuery) {\n return value;\n }\n const base = separatorIndex === -1 ? \"\" : value.slice(0, separatorIndex);\n const query = separatorIndex === -1 ? value : value.slice(separatorIndex + 1);\n const redactedParams = new URLSearchParams();\n for (const [name, paramValue] of new URLSearchParams(query)) {\n redactedParams.append(name, this.shouldRedactQueryParam(name) ? REDACTED : paramValue);\n }\n return separatorIndex === -1\n ? redactedParams.toString()\n : `${base}?${redactedParams.toString()}`;\n }\n\n redactHeaderValue(name: string, value: string | string[]): string | string[];\n redactHeaderValue(name: string, value: unknown): unknown;\n redactHeaderValue(name: string, value: unknown): unknown {\n if (this.shouldRedactHeader(name)) {\n return Array.isArray(value) ? [REDACTED] : REDACTED;\n }\n if (!URL_HEADER_NAMES.has(name.toLowerCase())) {\n return value;\n }\n if (typeof value === \"string\") {\n return this.redactQueryParams(value, false);\n }\n if (Array.isArray(value)) {\n return value.map((item) =>\n typeof item === \"string\" ? this.redactQueryParams(item, false) : item,\n );\n }\n return value;\n }\n\n // Whether a body is JSON is decided by a parse attempt, never by content type.\n redactBody(body: Buffer): string | Buffer {\n let text: string;\n try {\n text = utf8Decoder.decode(body);\n } catch {\n return body;\n }\n let data: JsonValue;\n try {\n data = JSON.parse(text);\n } catch {\n return text;\n }\n this.redactBodyFields(data);\n return JSON.stringify(data);\n }\n\n private shouldRedactHeader(name: string): boolean {\n // OpenTelemetry HTTP header attributes normalize hyphens to underscores.\n return (\n matchesAny(this.headerPatterns, name) ||\n (name.includes(\"_\") && matchesAny(this.headerPatterns, name.replaceAll(\"_\", \"-\")))\n );\n }\n\n private shouldRedactQueryParam(name: string): boolean {\n return matchesAny(this.queryParamPatterns, name);\n }\n\n private shouldRedactBodyField(name: string): boolean {\n return matchesAny(this.bodyFieldPatterns, name);\n }\n\n // Mutates in place, so callers pass only values they parsed themselves.\n private redactBodyFields(data: JsonValue): void {\n if (Array.isArray(data)) {\n for (const item of data) {\n this.redactBodyFields(item);\n }\n } else if (typeof data === \"object\" && data !== null) {\n for (const key of Object.keys(data)) {\n const value = data[key];\n if (typeof value === \"string\" && this.shouldRedactBodyField(key)) {\n data[key] = REDACTED;\n } else {\n this.redactBodyFields(value);\n }\n }\n }\n }\n}\n"],"mappings":";;;AASA,MAAa,WAAW;AAIxB,MAAM,mCAAmB,IAAI,IAAI,CAAC,YAAY,kBAAkB,CAAC;AAEjE,MAAM,cAAc,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC;AAI5D,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CAEA,cAAc;EACZ,MAAM,SAASA,eAAAA,UAAU;EACzB,KAAK,qBAAqBC,eAAAA,gBAAgBC,eAAAA,2BAA2B,OAAO,eAAe;EAC3F,KAAK,iBAAiBD,eAAAA,gBAAgBE,eAAAA,sBAAsB,OAAO,WAAW;EAC9E,KAAK,oBAAoBF,eAAAA,gBAAgBG,eAAAA,0BAA0B,OAAO,cAAc;CAC1F;CAIA,kBAAkB,OAAe,cAAc,MAAc;EAC3D,MAAM,iBAAiB,cAAc,KAAK,MAAM,QAAQ,GAAG;EAC3D,IAAI,mBAAmB,MAAM,CAAC,aAC5B,OAAO;EAET,MAAM,OAAO,mBAAmB,KAAK,KAAK,MAAM,MAAM,GAAG,cAAc;EACvE,MAAM,QAAQ,mBAAmB,KAAK,QAAQ,MAAM,MAAM,iBAAiB,CAAC;EAC5E,MAAM,iBAAiB,IAAI,gBAAgB;EAC3C,KAAK,MAAM,CAAC,MAAM,eAAe,IAAI,gBAAgB,KAAK,GACxD,eAAe,OAAO,MAAM,KAAK,uBAAuB,IAAI,IAAI,WAAW,UAAU;EAEvF,OAAO,mBAAmB,KACtB,eAAe,SAAS,IACxB,GAAG,KAAK,GAAG,eAAe,SAAS;CACzC;CAIA,kBAAkB,MAAc,OAAyB;EACvD,IAAI,KAAK,mBAAmB,IAAI,GAC9B,OAAO,MAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI;EAE7C,IAAI,CAAC,iBAAiB,IAAI,KAAK,YAAY,CAAC,GAC1C,OAAO;EAET,IAAI,OAAO,UAAU,UACnB,OAAO,KAAK,kBAAkB,OAAO,KAAK;EAE5C,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAChB,OAAO,SAAS,WAAW,KAAK,kBAAkB,MAAM,KAAK,IAAI,IACnE;EAEF,OAAO;CACT;CAGA,WAAW,MAA+B;EACxC,IAAI;EACJ,IAAI;GACF,OAAO,YAAY,OAAO,IAAI;EAChC,QAAQ;GACN,OAAO;EACT;EACA,IAAI;EACJ,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ;GACN,OAAO;EACT;EACA,KAAK,iBAAiB,IAAI;EAC1B,OAAO,KAAK,UAAU,IAAI;CAC5B;CAEA,mBAA2B,MAAuB;EAEhD,OACEC,eAAAA,WAAW,KAAK,gBAAgB,IAAI,KACnC,KAAK,SAAS,GAAG,KAAKA,eAAAA,WAAW,KAAK,gBAAgB,KAAK,WAAW,KAAK,GAAG,CAAC;CAEpF;CAEA,uBAA+B,MAAuB;EACpD,OAAOA,eAAAA,WAAW,KAAK,oBAAoB,IAAI;CACjD;CAEA,sBAA8B,MAAuB;EACnD,OAAOA,eAAAA,WAAW,KAAK,mBAAmB,IAAI;CAChD;CAGA,iBAAyB,MAAuB;EAC9C,IAAI,MAAM,QAAQ,IAAI,GACpB,KAAK,MAAM,QAAQ,MACjB,KAAK,iBAAiB,IAAI;OAEvB,IAAI,OAAO,SAAS,YAAY,SAAS,MAC9C,KAAK,MAAM,OAAO,OAAO,KAAK,IAAI,GAAG;GACnC,MAAM,QAAQ,KAAK;GACnB,IAAI,OAAO,UAAU,YAAY,KAAK,sBAAsB,GAAG,GAC7D,KAAK,OAAO;QAEZ,KAAK,iBAAiB,KAAK;EAE/B;CAEJ;AACF"}
package/dist/redaction.js CHANGED
@@ -42,10 +42,11 @@ var Redaction = class {
42
42
  } catch {
43
43
  return text;
44
44
  }
45
- return JSON.stringify(this.redactBodyFields(data));
45
+ this.redactBodyFields(data);
46
+ return JSON.stringify(data);
46
47
  }
47
48
  shouldRedactHeader(name) {
48
- return matchesAny(this.headerPatterns, name) || matchesAny(this.headerPatterns, name.replaceAll("_", "-"));
49
+ return matchesAny(this.headerPatterns, name) || name.includes("_") && matchesAny(this.headerPatterns, name.replaceAll("_", "-"));
49
50
  }
50
51
  shouldRedactQueryParam(name) {
51
52
  return matchesAny(this.queryParamPatterns, name);
@@ -54,9 +55,12 @@ var Redaction = class {
54
55
  return matchesAny(this.bodyFieldPatterns, name);
55
56
  }
56
57
  redactBodyFields(data) {
57
- if (Array.isArray(data)) return data.map((item) => this.redactBodyFields(item));
58
- if (typeof data === "object" && data !== null) return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, typeof value === "string" && this.shouldRedactBodyField(key) ? REDACTED : this.redactBodyFields(value)]));
59
- return data;
58
+ if (Array.isArray(data)) for (const item of data) this.redactBodyFields(item);
59
+ else if (typeof data === "object" && data !== null) for (const key of Object.keys(data)) {
60
+ const value = data[key];
61
+ if (typeof value === "string" && this.shouldRedactBodyField(key)) data[key] = REDACTED;
62
+ else this.redactBodyFields(value);
63
+ }
60
64
  }
61
65
  };
62
66
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"redaction.js","names":[],"sources":["../src/redaction.ts"],"sourcesContent":["import {\n compilePatterns,\n DEFAULT_MASK_BODY_FIELDS,\n DEFAULT_MASK_HEADERS,\n DEFAULT_MASK_QUERY_PARAMS,\n getConfig,\n matchesAny,\n} from \"./config.js\";\n\nexport const REDACTED = \"[REDACTED]\";\n\n// Location headers can contain secret query values, so redaction targets only\n// their query parameters.\nconst URL_HEADER_NAMES = new Set([\"location\", \"content-location\"]);\n\nconst utf8Decoder = new TextDecoder(\"utf-8\", { fatal: true });\n\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n\nexport class Redaction {\n private queryParamPatterns: RegExp[];\n private headerPatterns: RegExp[];\n private bodyFieldPatterns: RegExp[];\n\n constructor() {\n const config = getConfig();\n this.queryParamPatterns = compilePatterns(DEFAULT_MASK_QUERY_PARAMS, config.maskQueryParams);\n this.headerPatterns = compilePatterns(DEFAULT_MASK_HEADERS, config.maskHeaders);\n this.bodyFieldPatterns = compilePatterns(DEFAULT_MASK_BODY_FIELDS, config.maskBodyFields);\n }\n\n // Redacts matching parameter names in a path with a query, a full URL, or a\n // bare query string when `assumeQuery` is true.\n redactQueryParams(value: string, assumeQuery = true): string {\n const separatorIndex = assumeQuery ? -1 : value.indexOf(\"?\");\n if (separatorIndex === -1 && !assumeQuery) {\n return value;\n }\n const base = separatorIndex === -1 ? \"\" : value.slice(0, separatorIndex);\n const query = separatorIndex === -1 ? value : value.slice(separatorIndex + 1);\n const redactedParams = new URLSearchParams();\n for (const [name, paramValue] of new URLSearchParams(query)) {\n redactedParams.append(name, this.shouldRedactQueryParam(name) ? REDACTED : paramValue);\n }\n return separatorIndex === -1\n ? redactedParams.toString()\n : `${base}?${redactedParams.toString()}`;\n }\n\n redactHeaderValue(name: string, value: string | string[]): string | string[];\n redactHeaderValue(name: string, value: unknown): unknown;\n redactHeaderValue(name: string, value: unknown): unknown {\n if (this.shouldRedactHeader(name)) {\n return Array.isArray(value) ? [REDACTED] : REDACTED;\n }\n if (!URL_HEADER_NAMES.has(name.toLowerCase())) {\n return value;\n }\n if (typeof value === \"string\") {\n return this.redactQueryParams(value, false);\n }\n if (Array.isArray(value)) {\n return value.map((item) =>\n typeof item === \"string\" ? this.redactQueryParams(item, false) : item,\n );\n }\n return value;\n }\n\n // Whether a body is JSON is decided by a parse attempt, never by content type.\n redactBody(body: Buffer): string | Buffer {\n let text: string;\n try {\n text = utf8Decoder.decode(body);\n } catch {\n return body;\n }\n let data: JsonValue;\n try {\n data = JSON.parse(text);\n } catch {\n return text;\n }\n return JSON.stringify(this.redactBodyFields(data));\n }\n\n private shouldRedactHeader(name: string): boolean {\n // OpenTelemetry HTTP header attributes normalize hyphens to underscores.\n return (\n matchesAny(this.headerPatterns, name) ||\n matchesAny(this.headerPatterns, name.replaceAll(\"_\", \"-\"))\n );\n }\n\n private shouldRedactQueryParam(name: string): boolean {\n return matchesAny(this.queryParamPatterns, name);\n }\n\n private shouldRedactBodyField(name: string): boolean {\n return matchesAny(this.bodyFieldPatterns, name);\n }\n\n private redactBodyFields(data: JsonValue): JsonValue {\n if (Array.isArray(data)) {\n return data.map((item) => this.redactBodyFields(item));\n }\n if (typeof data === \"object\" && data !== null) {\n return Object.fromEntries(\n Object.entries(data).map(([key, value]) => [\n key,\n typeof value === \"string\" && this.shouldRedactBodyField(key)\n ? REDACTED\n : this.redactBodyFields(value),\n ]),\n );\n }\n return data;\n }\n}\n"],"mappings":";;AASA,MAAa,WAAW;AAIxB,MAAM,mCAAmB,IAAI,IAAI,CAAC,YAAY,kBAAkB,CAAC;AAEjE,MAAM,cAAc,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC;AAI5D,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CAEA,cAAc;EACZ,MAAM,SAAS,UAAU;EACzB,KAAK,qBAAqB,gBAAgB,2BAA2B,OAAO,eAAe;EAC3F,KAAK,iBAAiB,gBAAgB,sBAAsB,OAAO,WAAW;EAC9E,KAAK,oBAAoB,gBAAgB,0BAA0B,OAAO,cAAc;CAC1F;CAIA,kBAAkB,OAAe,cAAc,MAAc;EAC3D,MAAM,iBAAiB,cAAc,KAAK,MAAM,QAAQ,GAAG;EAC3D,IAAI,mBAAmB,MAAM,CAAC,aAC5B,OAAO;EAET,MAAM,OAAO,mBAAmB,KAAK,KAAK,MAAM,MAAM,GAAG,cAAc;EACvE,MAAM,QAAQ,mBAAmB,KAAK,QAAQ,MAAM,MAAM,iBAAiB,CAAC;EAC5E,MAAM,iBAAiB,IAAI,gBAAgB;EAC3C,KAAK,MAAM,CAAC,MAAM,eAAe,IAAI,gBAAgB,KAAK,GACxD,eAAe,OAAO,MAAM,KAAK,uBAAuB,IAAI,IAAI,WAAW,UAAU;EAEvF,OAAO,mBAAmB,KACtB,eAAe,SAAS,IACxB,GAAG,KAAK,GAAG,eAAe,SAAS;CACzC;CAIA,kBAAkB,MAAc,OAAyB;EACvD,IAAI,KAAK,mBAAmB,IAAI,GAC9B,OAAO,MAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI;EAE7C,IAAI,CAAC,iBAAiB,IAAI,KAAK,YAAY,CAAC,GAC1C,OAAO;EAET,IAAI,OAAO,UAAU,UACnB,OAAO,KAAK,kBAAkB,OAAO,KAAK;EAE5C,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAChB,OAAO,SAAS,WAAW,KAAK,kBAAkB,MAAM,KAAK,IAAI,IACnE;EAEF,OAAO;CACT;CAGA,WAAW,MAA+B;EACxC,IAAI;EACJ,IAAI;GACF,OAAO,YAAY,OAAO,IAAI;EAChC,QAAQ;GACN,OAAO;EACT;EACA,IAAI;EACJ,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ;GACN,OAAO;EACT;EACA,OAAO,KAAK,UAAU,KAAK,iBAAiB,IAAI,CAAC;CACnD;CAEA,mBAA2B,MAAuB;EAEhD,OACE,WAAW,KAAK,gBAAgB,IAAI,KACpC,WAAW,KAAK,gBAAgB,KAAK,WAAW,KAAK,GAAG,CAAC;CAE7D;CAEA,uBAA+B,MAAuB;EACpD,OAAO,WAAW,KAAK,oBAAoB,IAAI;CACjD;CAEA,sBAA8B,MAAuB;EACnD,OAAO,WAAW,KAAK,mBAAmB,IAAI;CAChD;CAEA,iBAAyB,MAA4B;EACnD,IAAI,MAAM,QAAQ,IAAI,GACpB,OAAO,KAAK,KAAK,SAAS,KAAK,iBAAiB,IAAI,CAAC;EAEvD,IAAI,OAAO,SAAS,YAAY,SAAS,MACvC,OAAO,OAAO,YACZ,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACzC,KACA,OAAO,UAAU,YAAY,KAAK,sBAAsB,GAAG,IACvD,WACA,KAAK,iBAAiB,KAAK,CACjC,CAAC,CACH;EAEF,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"redaction.js","names":[],"sources":["../src/redaction.ts"],"sourcesContent":["import {\n compilePatterns,\n DEFAULT_MASK_BODY_FIELDS,\n DEFAULT_MASK_HEADERS,\n DEFAULT_MASK_QUERY_PARAMS,\n getConfig,\n matchesAny,\n} from \"./config.js\";\n\nexport const REDACTED = \"[REDACTED]\";\n\n// Location headers can contain secret query values, so redaction targets only\n// their query parameters.\nconst URL_HEADER_NAMES = new Set([\"location\", \"content-location\"]);\n\nconst utf8Decoder = new TextDecoder(\"utf-8\", { fatal: true });\n\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n\nexport class Redaction {\n private queryParamPatterns: RegExp[];\n private headerPatterns: RegExp[];\n private bodyFieldPatterns: RegExp[];\n\n constructor() {\n const config = getConfig();\n this.queryParamPatterns = compilePatterns(DEFAULT_MASK_QUERY_PARAMS, config.maskQueryParams);\n this.headerPatterns = compilePatterns(DEFAULT_MASK_HEADERS, config.maskHeaders);\n this.bodyFieldPatterns = compilePatterns(DEFAULT_MASK_BODY_FIELDS, config.maskBodyFields);\n }\n\n // Redacts matching parameter names in a path with a query, a full URL, or a\n // bare query string when `assumeQuery` is true.\n redactQueryParams(value: string, assumeQuery = true): string {\n const separatorIndex = assumeQuery ? -1 : value.indexOf(\"?\");\n if (separatorIndex === -1 && !assumeQuery) {\n return value;\n }\n const base = separatorIndex === -1 ? \"\" : value.slice(0, separatorIndex);\n const query = separatorIndex === -1 ? value : value.slice(separatorIndex + 1);\n const redactedParams = new URLSearchParams();\n for (const [name, paramValue] of new URLSearchParams(query)) {\n redactedParams.append(name, this.shouldRedactQueryParam(name) ? REDACTED : paramValue);\n }\n return separatorIndex === -1\n ? redactedParams.toString()\n : `${base}?${redactedParams.toString()}`;\n }\n\n redactHeaderValue(name: string, value: string | string[]): string | string[];\n redactHeaderValue(name: string, value: unknown): unknown;\n redactHeaderValue(name: string, value: unknown): unknown {\n if (this.shouldRedactHeader(name)) {\n return Array.isArray(value) ? [REDACTED] : REDACTED;\n }\n if (!URL_HEADER_NAMES.has(name.toLowerCase())) {\n return value;\n }\n if (typeof value === \"string\") {\n return this.redactQueryParams(value, false);\n }\n if (Array.isArray(value)) {\n return value.map((item) =>\n typeof item === \"string\" ? this.redactQueryParams(item, false) : item,\n );\n }\n return value;\n }\n\n // Whether a body is JSON is decided by a parse attempt, never by content type.\n redactBody(body: Buffer): string | Buffer {\n let text: string;\n try {\n text = utf8Decoder.decode(body);\n } catch {\n return body;\n }\n let data: JsonValue;\n try {\n data = JSON.parse(text);\n } catch {\n return text;\n }\n this.redactBodyFields(data);\n return JSON.stringify(data);\n }\n\n private shouldRedactHeader(name: string): boolean {\n // OpenTelemetry HTTP header attributes normalize hyphens to underscores.\n return (\n matchesAny(this.headerPatterns, name) ||\n (name.includes(\"_\") && matchesAny(this.headerPatterns, name.replaceAll(\"_\", \"-\")))\n );\n }\n\n private shouldRedactQueryParam(name: string): boolean {\n return matchesAny(this.queryParamPatterns, name);\n }\n\n private shouldRedactBodyField(name: string): boolean {\n return matchesAny(this.bodyFieldPatterns, name);\n }\n\n // Mutates in place, so callers pass only values they parsed themselves.\n private redactBodyFields(data: JsonValue): void {\n if (Array.isArray(data)) {\n for (const item of data) {\n this.redactBodyFields(item);\n }\n } else if (typeof data === \"object\" && data !== null) {\n for (const key of Object.keys(data)) {\n const value = data[key];\n if (typeof value === \"string\" && this.shouldRedactBodyField(key)) {\n data[key] = REDACTED;\n } else {\n this.redactBodyFields(value);\n }\n }\n }\n }\n}\n"],"mappings":";;AASA,MAAa,WAAW;AAIxB,MAAM,mCAAmB,IAAI,IAAI,CAAC,YAAY,kBAAkB,CAAC;AAEjE,MAAM,cAAc,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC;AAI5D,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CAEA,cAAc;EACZ,MAAM,SAAS,UAAU;EACzB,KAAK,qBAAqB,gBAAgB,2BAA2B,OAAO,eAAe;EAC3F,KAAK,iBAAiB,gBAAgB,sBAAsB,OAAO,WAAW;EAC9E,KAAK,oBAAoB,gBAAgB,0BAA0B,OAAO,cAAc;CAC1F;CAIA,kBAAkB,OAAe,cAAc,MAAc;EAC3D,MAAM,iBAAiB,cAAc,KAAK,MAAM,QAAQ,GAAG;EAC3D,IAAI,mBAAmB,MAAM,CAAC,aAC5B,OAAO;EAET,MAAM,OAAO,mBAAmB,KAAK,KAAK,MAAM,MAAM,GAAG,cAAc;EACvE,MAAM,QAAQ,mBAAmB,KAAK,QAAQ,MAAM,MAAM,iBAAiB,CAAC;EAC5E,MAAM,iBAAiB,IAAI,gBAAgB;EAC3C,KAAK,MAAM,CAAC,MAAM,eAAe,IAAI,gBAAgB,KAAK,GACxD,eAAe,OAAO,MAAM,KAAK,uBAAuB,IAAI,IAAI,WAAW,UAAU;EAEvF,OAAO,mBAAmB,KACtB,eAAe,SAAS,IACxB,GAAG,KAAK,GAAG,eAAe,SAAS;CACzC;CAIA,kBAAkB,MAAc,OAAyB;EACvD,IAAI,KAAK,mBAAmB,IAAI,GAC9B,OAAO,MAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI;EAE7C,IAAI,CAAC,iBAAiB,IAAI,KAAK,YAAY,CAAC,GAC1C,OAAO;EAET,IAAI,OAAO,UAAU,UACnB,OAAO,KAAK,kBAAkB,OAAO,KAAK;EAE5C,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAChB,OAAO,SAAS,WAAW,KAAK,kBAAkB,MAAM,KAAK,IAAI,IACnE;EAEF,OAAO;CACT;CAGA,WAAW,MAA+B;EACxC,IAAI;EACJ,IAAI;GACF,OAAO,YAAY,OAAO,IAAI;EAChC,QAAQ;GACN,OAAO;EACT;EACA,IAAI;EACJ,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ;GACN,OAAO;EACT;EACA,KAAK,iBAAiB,IAAI;EAC1B,OAAO,KAAK,UAAU,IAAI;CAC5B;CAEA,mBAA2B,MAAuB;EAEhD,OACE,WAAW,KAAK,gBAAgB,IAAI,KACnC,KAAK,SAAS,GAAG,KAAK,WAAW,KAAK,gBAAgB,KAAK,WAAW,KAAK,GAAG,CAAC;CAEpF;CAEA,uBAA+B,MAAuB;EACpD,OAAO,WAAW,KAAK,oBAAoB,IAAI;CACjD;CAEA,sBAA8B,MAAuB;EACnD,OAAO,WAAW,KAAK,mBAAmB,IAAI;CAChD;CAGA,iBAAyB,MAAuB;EAC9C,IAAI,MAAM,QAAQ,IAAI,GACpB,KAAK,MAAM,QAAQ,MACjB,KAAK,iBAAiB,IAAI;OAEvB,IAAI,OAAO,SAAS,YAAY,SAAS,MAC9C,KAAK,MAAM,OAAO,OAAO,KAAK,IAAI,GAAG;GACnC,MAAM,QAAQ,KAAK;GACnB,IAAI,OAAO,UAAU,YAAY,KAAK,sBAAsB,GAAG,GAC7D,KAAK,OAAO;QAEZ,KAAK,iBAAiB,KAAK;EAE/B;CAEJ;AACF"}
@@ -2,28 +2,26 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_logger = require("./logger.cjs");
3
3
  const require_config = require("./config.cjs");
4
4
  const require_context = require("./context.cjs");
5
+ const require_spanProcessor = require("./spanProcessor.cjs");
5
6
  const require_requestAttributes = require("./requestAttributes.cjs");
6
7
  const require_exceptions = require("./exceptions.cjs");
7
8
  const require_serverErrors = require("./serverErrors.cjs");
8
- const require_spanProcessor = require("./spanProcessor.cjs");
9
9
  const require_validationErrors = require("./validationErrors.cjs");
10
+ const require_activation = require("./activation.cjs");
10
11
  const require_consumers = require("./consumers.cjs");
11
12
  let _opentelemetry_api = require("@opentelemetry/api");
12
13
  let _opentelemetry_core = require("@opentelemetry/core");
13
14
  //#region src/requestObservation.ts
14
15
  function resolveHttpRequestStartAttributes(input) {
15
16
  const attributes = { "http.request.method": input.method };
16
- const optionalAttributes = [
17
- ["url.path", input.path],
18
- ["url.query", input.query],
19
- ["url.scheme", input.scheme],
20
- ["url.full", input.fullUrl],
21
- ["server.address", input.serverAddress],
22
- ["client.address", input.clientAddress],
23
- ["user_agent.original", input.userAgent],
24
- ["http.request.body.size", input.requestBodySize]
25
- ];
26
- for (const [name, value] of optionalAttributes) if (value !== void 0) attributes[name] = value;
17
+ if (input.path !== void 0) attributes["url.path"] = input.path;
18
+ if (input.query !== void 0) attributes["url.query"] = input.query;
19
+ if (input.scheme !== void 0) attributes["url.scheme"] = input.scheme;
20
+ if (input.fullUrl !== void 0) attributes["url.full"] = input.fullUrl;
21
+ if (input.serverAddress !== void 0) attributes["server.address"] = input.serverAddress;
22
+ if (input.clientAddress !== void 0) attributes["client.address"] = input.clientAddress;
23
+ if (input.userAgent !== void 0) attributes["user_agent.original"] = input.userAgent;
24
+ if (input.requestBodySize !== void 0) attributes["http.request.body.size"] = input.requestBodySize;
27
25
  return attributes;
28
26
  }
29
27
  function startRequestObservation(options) {
@@ -42,7 +40,7 @@ function startRequestObservation(options) {
42
40
  requestContext = require_context.withRequestHolders(activeContext, spanHandle, requestRecord);
43
41
  } else if (activeSpan && !activeSpan.isRecording() && !activeSpan.spanContext().isRemote) {
44
42
  requestRecord.dropReason = resolveUnavailableSpanDropReason();
45
- if (requestRecord.dropReason === "sampled-out") warnAboutNonRecordingServerSpan();
43
+ if (requestRecord.dropReason === "sampled-out") warnIfUserSamplerDroppedServerSpan();
46
44
  requestContext = require_context.withRequestHolders(activeContext, spanHandle, requestRecord);
47
45
  } else {
48
46
  requestContext = require_context.withRequestHolders(extractedContext, spanHandle, requestRecord);
@@ -52,7 +50,7 @@ function startRequestObservation(options) {
52
50
  }, requestContext);
53
51
  if (!ownSpan.isRecording()) {
54
52
  requestRecord.dropReason = resolveUnavailableSpanDropReason();
55
- if (requestRecord.dropReason === "sampled-out") warnAboutNonRecordingServerSpan();
53
+ if (requestRecord.dropReason === "sampled-out") warnIfUserSamplerDroppedServerSpan();
56
54
  } else {
57
55
  spanHandle.span = ownSpan;
58
56
  spanHandle.ownSpan = ownSpan;
@@ -138,7 +136,10 @@ function finalizeRequestObservationWithError(options) {
138
136
  }
139
137
  function getHeaderValue(headers, name) {
140
138
  if (isWebHeaders(headers)) return headers.get(name) ?? void 0;
141
- for (const [headerName, value] of Object.entries(headers)) if (headerName.toLowerCase() === name) return Array.isArray(value) ? value.join(",") : value?.toString();
139
+ for (const headerName of Object.keys(headers)) if (headerName.toLowerCase() === name) {
140
+ const value = headers[headerName];
141
+ return Array.isArray(value) ? value.join(",") : value?.toString();
142
+ }
142
143
  }
143
144
  function normalizeHeaders(headers) {
144
145
  const normalized = {};
@@ -151,13 +152,17 @@ function normalizeHeaders(headers) {
151
152
  }
152
153
  return normalized;
153
154
  }
154
- for (const [name, value] of Object.entries(headers)) if (value !== void 0) normalized[name.toLowerCase()] = Array.isArray(value) ? value : String(value);
155
+ for (const name of Object.keys(headers)) {
156
+ const value = headers[name];
157
+ if (value !== void 0) normalized[name.toLowerCase()] = Array.isArray(value) ? value : String(value);
158
+ }
155
159
  return normalized;
156
160
  }
157
161
  function isWebHeaders(headers) {
158
162
  return typeof headers[Symbol.iterator] === "function";
159
163
  }
160
- function warnAboutNonRecordingServerSpan() {
164
+ function warnIfUserSamplerDroppedServerSpan() {
165
+ if (require_activation.getActivationHandles()?.tracerProvider) return;
161
166
  require_logger.logWarning("The OpenTelemetry sampler did not sample the SERVER span of a request. Only sampled requests are exported to Apitally as traces and request logs. Request metrics include all requests.");
162
167
  }
163
168
  function warnAboutUnattachedSpanProcessor() {