@elto/telemetry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +284 -0
- package/dist/index.cjs +19 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +91 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.cjs +204 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +91 -0
- package/dist/node/index.d.ts +91 -0
- package/dist/node/index.js +175 -0
- package/dist/node/index.js.map +1 -0
- package/dist/web/index.cjs +278 -0
- package/dist/web/index.cjs.map +1 -0
- package/dist/web/index.d.cts +230 -0
- package/dist/web/index.d.ts +230 -0
- package/dist/web/index.js +255 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/node/index.ts
|
|
21
|
+
var node_exports = {};
|
|
22
|
+
__export(node_exports, {
|
|
23
|
+
DEFAULT_OTEL_ENDPOINT: () => DEFAULT_OTEL_ENDPOINT,
|
|
24
|
+
createEndpoints: () => createEndpoints,
|
|
25
|
+
createNodeOtelLayer: () => createNodeOtelLayer
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(node_exports);
|
|
28
|
+
|
|
29
|
+
// src/node/sdk.ts
|
|
30
|
+
var import_opentelemetry = require("@effect/opentelemetry");
|
|
31
|
+
var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
|
|
32
|
+
var import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
|
|
33
|
+
var import_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
|
|
34
|
+
var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
35
|
+
var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
36
|
+
var import_sdk_logs = require("@opentelemetry/sdk-logs");
|
|
37
|
+
|
|
38
|
+
// src/common/span-processors.ts
|
|
39
|
+
var DROP_ATTRIBUTE_KEY = "telemetry.drop";
|
|
40
|
+
var REDACTED_VALUE = "[redacted]";
|
|
41
|
+
var STATEMENT_MAX_LENGTH = 12;
|
|
42
|
+
var STATEMENT_HEAD_LENGTH = 3;
|
|
43
|
+
var STATEMENT_TAIL_LENGTH = 3;
|
|
44
|
+
var PII_ATTRIBUTE_KEYS = /* @__PURE__ */ new Set(["user.email", "note.id", "profile.id"]);
|
|
45
|
+
var getAttributes = (span) => span.attributes ?? {};
|
|
46
|
+
var setAttribute = (span, key, value) => {
|
|
47
|
+
const target = span;
|
|
48
|
+
if (typeof target.setAttribute === "function") {
|
|
49
|
+
target.setAttribute(key, value);
|
|
50
|
+
}
|
|
51
|
+
if (target.attributes) {
|
|
52
|
+
target.attributes[key] = value;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var hashString = (value) => {
|
|
56
|
+
let hash = 2166136261;
|
|
57
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
58
|
+
hash ^= value.charCodeAt(index);
|
|
59
|
+
hash = Math.imul(hash, 16777619);
|
|
60
|
+
}
|
|
61
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
62
|
+
};
|
|
63
|
+
var hashAttributeValue = (value) => {
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return hashString(value.map((item) => String(item)).join("|"));
|
|
66
|
+
}
|
|
67
|
+
return hashString(String(value));
|
|
68
|
+
};
|
|
69
|
+
var redactConnectionString = (value) => value.replace(/\/\/([^@/]+)@/, "//***:***@");
|
|
70
|
+
var stripUrlQuery = (value) => value.split("?")[0] ?? value;
|
|
71
|
+
var shortenStatement = (statement) => {
|
|
72
|
+
if (statement.length <= STATEMENT_MAX_LENGTH) {
|
|
73
|
+
return statement;
|
|
74
|
+
}
|
|
75
|
+
const head = statement.slice(0, STATEMENT_HEAD_LENGTH);
|
|
76
|
+
const tail = statement.slice(-STATEMENT_TAIL_LENGTH);
|
|
77
|
+
const removed = statement.length - (STATEMENT_HEAD_LENGTH + STATEMENT_TAIL_LENGTH);
|
|
78
|
+
return `${head}...${tail} (+ ${removed} characters)`;
|
|
79
|
+
};
|
|
80
|
+
var shouldDropSpan = (span) => {
|
|
81
|
+
const attributes = getAttributes(span);
|
|
82
|
+
const drop = attributes[DROP_ATTRIBUTE_KEY];
|
|
83
|
+
return drop === true || drop === "true";
|
|
84
|
+
};
|
|
85
|
+
var sanitizeAttributes = (span, piiAttributes) => {
|
|
86
|
+
const attributes = getAttributes(span);
|
|
87
|
+
let redactions = 0;
|
|
88
|
+
let transformed = 0;
|
|
89
|
+
const piiKeys = piiAttributes ?? PII_ATTRIBUTE_KEYS;
|
|
90
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
91
|
+
if (value === void 0) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (piiKeys.has(key)) {
|
|
95
|
+
setAttribute(span, `${key}_hash`, hashAttributeValue(value));
|
|
96
|
+
setAttribute(span, key, REDACTED_VALUE);
|
|
97
|
+
redactions += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (key === "db.connection_string" && typeof value === "string") {
|
|
101
|
+
setAttribute(span, "db.connection_string_redacted", redactConnectionString(value));
|
|
102
|
+
setAttribute(span, key, REDACTED_VALUE);
|
|
103
|
+
redactions += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (key === "db.statement" && typeof value === "string") {
|
|
107
|
+
const shortened = shortenStatement(value);
|
|
108
|
+
if (shortened !== value) {
|
|
109
|
+
setAttribute(span, key, shortened);
|
|
110
|
+
transformed += 1;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (key === "http.url" && typeof value === "string") {
|
|
115
|
+
const sanitized = stripUrlQuery(value);
|
|
116
|
+
if (sanitized !== value) {
|
|
117
|
+
setAttribute(span, key, sanitized);
|
|
118
|
+
transformed += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const statusCode = attributes["http.status_code"];
|
|
123
|
+
if (typeof statusCode === "number") {
|
|
124
|
+
setAttribute(span, "http.status_category", `${Math.floor(statusCode / 100)}xx`);
|
|
125
|
+
transformed += 1;
|
|
126
|
+
}
|
|
127
|
+
if (redactions > 0 || transformed > 0) {
|
|
128
|
+
setAttribute(span, "telemetry.redactions", redactions);
|
|
129
|
+
setAttribute(span, "telemetry.transformations", transformed);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var CustomSpanProcessor = class {
|
|
133
|
+
constructor(delegate, piiAttributes) {
|
|
134
|
+
this.delegate = delegate;
|
|
135
|
+
this.piiAttributes = piiAttributes;
|
|
136
|
+
}
|
|
137
|
+
onStart(span, parentContext) {
|
|
138
|
+
sanitizeAttributes(span, this.piiAttributes);
|
|
139
|
+
this.delegate.onStart(span, parentContext);
|
|
140
|
+
}
|
|
141
|
+
onEnd(span) {
|
|
142
|
+
sanitizeAttributes(span, this.piiAttributes);
|
|
143
|
+
if (shouldDropSpan(span)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.delegate.onEnd(span);
|
|
147
|
+
}
|
|
148
|
+
shutdown() {
|
|
149
|
+
return this.delegate.shutdown();
|
|
150
|
+
}
|
|
151
|
+
forceFlush() {
|
|
152
|
+
return this.delegate.forceFlush();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var createCustomSpanProcessor = (delegate, piiConfig) => new CustomSpanProcessor(delegate, piiConfig?.piiAttributeKeys);
|
|
156
|
+
|
|
157
|
+
// src/node/sdk.ts
|
|
158
|
+
var DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
159
|
+
function createEndpoints(baseUrl = DEFAULT_OTEL_ENDPOINT) {
|
|
160
|
+
return {
|
|
161
|
+
traces: `${baseUrl}/v1/traces`,
|
|
162
|
+
logs: `${baseUrl}/v1/logs`,
|
|
163
|
+
metrics: `${baseUrl}/v1/metrics`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function createNodeOtelLayer(config) {
|
|
167
|
+
const { resource, endpoints, metricExportIntervalMs = 1e4 } = config;
|
|
168
|
+
if (!endpoints.metrics) {
|
|
169
|
+
throw new Error("metrics endpoint is required for Node.js SDK");
|
|
170
|
+
}
|
|
171
|
+
const metricsUrl = endpoints.metrics;
|
|
172
|
+
return import_opentelemetry.NodeSdk.layer(() => ({
|
|
173
|
+
resource: {
|
|
174
|
+
serviceName: resource.serviceName,
|
|
175
|
+
serviceVersion: resource.serviceVersion
|
|
176
|
+
},
|
|
177
|
+
spanProcessor: createCustomSpanProcessor(
|
|
178
|
+
new import_sdk_trace_base.BatchSpanProcessor(
|
|
179
|
+
new import_exporter_trace_otlp_http.OTLPTraceExporter({
|
|
180
|
+
url: endpoints.traces
|
|
181
|
+
})
|
|
182
|
+
),
|
|
183
|
+
config.piiConfig
|
|
184
|
+
),
|
|
185
|
+
metricReader: new import_sdk_metrics.PeriodicExportingMetricReader({
|
|
186
|
+
exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter({
|
|
187
|
+
url: metricsUrl
|
|
188
|
+
}),
|
|
189
|
+
exportIntervalMillis: metricExportIntervalMs
|
|
190
|
+
}),
|
|
191
|
+
logRecordProcessor: new import_sdk_logs.BatchLogRecordProcessor(
|
|
192
|
+
new import_exporter_logs_otlp_http.OTLPLogExporter({
|
|
193
|
+
url: endpoints.logs
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
199
|
+
0 && (module.exports = {
|
|
200
|
+
DEFAULT_OTEL_ENDPOINT,
|
|
201
|
+
createEndpoints,
|
|
202
|
+
createNodeOtelLayer
|
|
203
|
+
});
|
|
204
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/node/index.ts","../../src/node/sdk.ts","../../src/common/span-processors.ts"],"sourcesContent":["/**\n * Node.js OpenTelemetry integration for Effect\n *\n * This module provides utilities for configuring OpenTelemetry in Node.js applications.\n *\n * @example\n * ```typescript\n * import { createNodeOtelLayer, createEndpoints } from \"@elto/telemetry/node\";\n *\n * const OtelLive = createNodeOtelLayer({\n * resource: {\n * serviceName: \"api-server\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: createEndpoints(\"http://localhost:4318\"),\n * });\n * ```\n *\n * @packageDocumentation\n */\nexport { createNodeOtelLayer, createEndpoints, DEFAULT_OTEL_ENDPOINT } from \"./sdk.js\";\n\nexport type { NodeSdkConfig, ResourceConfig, EndpointConfig } from \"../common/types.js\";\n","import { NodeSdk } from \"@effect/opentelemetry\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport { PeriodicExportingMetricReader } from \"@opentelemetry/sdk-metrics\";\nimport { BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { createCustomSpanProcessor } from \"../common/span-processors.js\";\nimport type { NodeSdkConfig, EndpointConfig } from \"../common/types.js\";\n\n/**\n * Default OpenTelemetry collector endpoint\n */\nexport const DEFAULT_OTEL_ENDPOINT = \"http://localhost:4318\";\n\n/**\n * Create endpoint URLs for OTLP exporters\n *\n * @param baseUrl - Base URL of the OpenTelemetry collector (e.g., \"http://localhost:4318\")\n * @returns Endpoint configuration object\n *\n * @example\n * ```typescript\n * const endpoints = createEndpoints(\"http://localhost:4318\");\n * // Returns: {\n * // traces: \"http://localhost:4318/v1/traces\",\n * // logs: \"http://localhost:4318/v1/logs\",\n * // metrics: \"http://localhost:4318/v1/metrics\"\n * // }\n * ```\n */\nexport function createEndpoints(baseUrl: string = DEFAULT_OTEL_ENDPOINT): EndpointConfig {\n return {\n traces: `${baseUrl}/v1/traces`,\n logs: `${baseUrl}/v1/logs`,\n metrics: `${baseUrl}/v1/metrics`,\n };\n}\n\n/**\n * Create an OpenTelemetry Layer for Node.js applications\n *\n * This layer configures:\n * - OTLP HTTP exporter for traces (to Jaeger)\n * - OTLP HTTP exporter for metrics (to Prometheus)\n * - OTLP HTTP exporter for logs (to Loki)\n * - Custom span processor for redaction/filtering before export\n * - Batch processors for efficient batching\n *\n * @param config - Configuration object\n * @returns Effect Layer for OpenTelemetry\n *\n * @example\n * ```typescript\n * import { createNodeOtelLayer, createEndpoints } from \"@elto/telemetry/node\";\n *\n * const OtelLive = createNodeOtelLayer({\n * resource: {\n * serviceName: \"api-server\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: createEndpoints(\"http://localhost:4318\"),\n * metricExportIntervalMs: 10000, // optional, default 10000\n * });\n * ```\n */\nexport function createNodeOtelLayer(config: NodeSdkConfig) {\n const { resource, endpoints, metricExportIntervalMs = 10000 } = config;\n\n if (!endpoints.metrics) {\n throw new Error(\"metrics endpoint is required for Node.js SDK\");\n }\n\n const metricsUrl = endpoints.metrics;\n\n return NodeSdk.layer(() => ({\n resource: {\n serviceName: resource.serviceName,\n serviceVersion: resource.serviceVersion,\n },\n spanProcessor: createCustomSpanProcessor(\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: endpoints.traces,\n }),\n ),\n config.piiConfig,\n ),\n metricReader: new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: metricsUrl,\n }),\n exportIntervalMillis: metricExportIntervalMs,\n }),\n logRecordProcessor: new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: endpoints.logs,\n }),\n ),\n }));\n}\n","import type { AttributeValue, Context } from \"@opentelemetry/api\";\nimport type { ReadableSpan, Span, SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport type { PIIConfig } from \"./types\";\n\n/**\n * Default span processor used by @elto/telemetry to normalize and sanitize\n * attributes before export. It is intentionally small and opinionated so\n * demo traces stay safe and readable.\n */\ntype SpanAttributes = Record<string, AttributeValue | undefined>;\n\nconst DROP_ATTRIBUTE_KEY = \"telemetry.drop\";\nconst REDACTED_VALUE = \"[redacted]\";\nconst STATEMENT_MAX_LENGTH = 12;\nconst STATEMENT_HEAD_LENGTH = 3;\nconst STATEMENT_TAIL_LENGTH = 3;\n\nconst PII_ATTRIBUTE_KEYS = new Set([\"user.email\", \"note.id\", \"profile.id\"]);\n\nconst getAttributes = (span: Span | ReadableSpan): SpanAttributes =>\n (span as { attributes?: SpanAttributes }).attributes ?? {};\n\nconst setAttribute = (span: Span | ReadableSpan, key: string, value: AttributeValue) => {\n const target = span as {\n setAttribute?: (attributeKey: string, attributeValue: AttributeValue) => void;\n attributes?: SpanAttributes;\n };\n\n if (typeof target.setAttribute === \"function\") {\n target.setAttribute(key, value);\n }\n\n if (target.attributes) {\n target.attributes[key] = value;\n }\n};\n\nconst hashString = (value: string) => {\n let hash = 2166136261;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n};\n\nconst hashAttributeValue = (value: AttributeValue) => {\n if (Array.isArray(value)) {\n return hashString(value.map((item) => String(item)).join(\"|\"));\n }\n return hashString(String(value));\n};\n\nconst redactConnectionString = (value: string) => value.replace(/\\/\\/([^@/]+)@/, \"//***:***@\");\n\nconst stripUrlQuery = (value: string) => value.split(\"?\")[0] ?? value;\n\n/**\n * Shorten a long SQL statement to a leading head + tail with a marker that\n * explains how many characters were removed.\n */\nconst shortenStatement = (statement: string) => {\n if (statement.length <= STATEMENT_MAX_LENGTH) {\n return statement;\n }\n\n const head = statement.slice(0, STATEMENT_HEAD_LENGTH);\n const tail = statement.slice(-STATEMENT_TAIL_LENGTH);\n const removed = statement.length - (STATEMENT_HEAD_LENGTH + STATEMENT_TAIL_LENGTH);\n return `${head}...${tail} (+ ${removed} characters)`;\n};\n\nconst shouldDropSpan = (span: ReadableSpan) => {\n const attributes = getAttributes(span);\n const drop = attributes[DROP_ATTRIBUTE_KEY];\n return drop === true || drop === \"true\";\n};\n\nconst sanitizeAttributes = (span: Span | ReadableSpan, piiAttributes?: Set<string>) => {\n const attributes = getAttributes(span);\n let redactions = 0;\n let transformed = 0;\n\n const piiKeys = piiAttributes ?? PII_ATTRIBUTE_KEYS;\n for (const [key, value] of Object.entries(attributes)) {\n if (value === undefined) {\n continue;\n }\n\n if (piiKeys.has(key)) {\n setAttribute(span, `${key}_hash`, hashAttributeValue(value));\n setAttribute(span, key, REDACTED_VALUE);\n redactions += 1;\n continue;\n }\n\n if (key === \"db.connection_string\" && typeof value === \"string\") {\n setAttribute(span, \"db.connection_string_redacted\", redactConnectionString(value));\n setAttribute(span, key, REDACTED_VALUE);\n redactions += 1;\n continue;\n }\n\n if (key === \"db.statement\" && typeof value === \"string\") {\n const shortened = shortenStatement(value);\n if (shortened !== value) {\n setAttribute(span, key, shortened);\n transformed += 1;\n }\n continue;\n }\n\n if (key === \"http.url\" && typeof value === \"string\") {\n const sanitized = stripUrlQuery(value);\n if (sanitized !== value) {\n setAttribute(span, key, sanitized);\n transformed += 1;\n }\n }\n }\n\n const statusCode = attributes[\"http.status_code\"];\n if (typeof statusCode === \"number\") {\n setAttribute(span, \"http.status_category\", `${Math.floor(statusCode / 100)}xx`);\n transformed += 1;\n }\n\n if (redactions > 0 || transformed > 0) {\n setAttribute(span, \"telemetry.redactions\", redactions);\n setAttribute(span, \"telemetry.transformations\", transformed);\n }\n};\n\n/**\n * A SpanProcessor that applies redaction, attribute normalization, and optional\n * dropping before delegating to another processor (typically a BatchSpanProcessor).\n *\n * Behavior:\n * - Redacts `user.email`, `note.id`, and `profile.id` while adding `<key>_hash`.\n * - Redacts `db.connection_string` and stores a sanitized copy in\n * `db.connection_string_redacted`.\n * - Shortens `db.statement` when it exceeds the configured threshold.\n * - Strips query strings from `http.url`.\n * - Adds `http.status_category` when `http.status_code` is present.\n * - Adds `telemetry.redactions` and `telemetry.transformations` counters.\n * - Drops spans when `telemetry.drop` is `true` or `\"true\"`.\n *\n * @param delegate - The SpanProcessor to delegate to after sanitization\n * @param piiAttributes - Optional set of span attribute keys whose values will be redacted.\n * If provided, this overrides the default {@link PII_ATTRIBUTE_KEYS} (`user.email`, `note.id`, `profile.id`).\n * For each matching key, the original value is replaced with `[redacted]` and a hash\n * is stored in a `<key>_hash` attribute.\n */\nexport class CustomSpanProcessor implements SpanProcessor {\n constructor(\n private readonly delegate: SpanProcessor,\n private readonly piiAttributes?: Set<string>,\n ) {}\n\n onStart(span: Span, parentContext: Context): void {\n sanitizeAttributes(span, this.piiAttributes);\n this.delegate.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n sanitizeAttributes(span, this.piiAttributes);\n if (shouldDropSpan(span)) {\n return;\n }\n this.delegate.onEnd(span);\n }\n\n shutdown(): Promise<void> {\n return this.delegate.shutdown();\n }\n\n forceFlush(): Promise<void> {\n return this.delegate.forceFlush();\n }\n}\n\n/**\n * Convenience factory for wrapping a delegate SpanProcessor with the\n * @elto/telemetry custom sanitation behavior.\n */\nexport const createCustomSpanProcessor = (delegate: SpanProcessor, piiConfig?: PIIConfig) =>\n new CustomSpanProcessor(delegate, piiConfig?.piiAttributeKeys);\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as effect_Layer from 'effect/Layer';
|
|
2
|
+
import * as _effect_opentelemetry_Resource from '@effect/opentelemetry/Resource';
|
|
3
|
+
|
|
4
|
+
interface PIIConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Set of span attribute keys whose values will be redacted.
|
|
7
|
+
*/
|
|
8
|
+
piiAttributeKeys?: Set<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Redacted value placeholder.
|
|
11
|
+
*
|
|
12
|
+
* @default "[redacted]"
|
|
13
|
+
*/
|
|
14
|
+
redactedValue?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resource configuration for OpenTelemetry
|
|
18
|
+
*/
|
|
19
|
+
interface ResourceConfig {
|
|
20
|
+
serviceName: string;
|
|
21
|
+
serviceVersion: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Endpoint configuration for OpenTelemetry exporters
|
|
25
|
+
*/
|
|
26
|
+
interface EndpointConfig {
|
|
27
|
+
traces: string;
|
|
28
|
+
logs: string;
|
|
29
|
+
metrics?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Configuration for Node.js OpenTelemetry SDK
|
|
33
|
+
*/
|
|
34
|
+
interface NodeSdkConfig {
|
|
35
|
+
resource: ResourceConfig;
|
|
36
|
+
endpoints: EndpointConfig;
|
|
37
|
+
metricExportIntervalMs?: number;
|
|
38
|
+
piiConfig?: PIIConfig;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Default OpenTelemetry collector endpoint
|
|
43
|
+
*/
|
|
44
|
+
declare const DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
45
|
+
/**
|
|
46
|
+
* Create endpoint URLs for OTLP exporters
|
|
47
|
+
*
|
|
48
|
+
* @param baseUrl - Base URL of the OpenTelemetry collector (e.g., "http://localhost:4318")
|
|
49
|
+
* @returns Endpoint configuration object
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const endpoints = createEndpoints("http://localhost:4318");
|
|
54
|
+
* // Returns: {
|
|
55
|
+
* // traces: "http://localhost:4318/v1/traces",
|
|
56
|
+
* // logs: "http://localhost:4318/v1/logs",
|
|
57
|
+
* // metrics: "http://localhost:4318/v1/metrics"
|
|
58
|
+
* // }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
declare function createEndpoints(baseUrl?: string): EndpointConfig;
|
|
62
|
+
/**
|
|
63
|
+
* Create an OpenTelemetry Layer for Node.js applications
|
|
64
|
+
*
|
|
65
|
+
* This layer configures:
|
|
66
|
+
* - OTLP HTTP exporter for traces (to Jaeger)
|
|
67
|
+
* - OTLP HTTP exporter for metrics (to Prometheus)
|
|
68
|
+
* - OTLP HTTP exporter for logs (to Loki)
|
|
69
|
+
* - Custom span processor for redaction/filtering before export
|
|
70
|
+
* - Batch processors for efficient batching
|
|
71
|
+
*
|
|
72
|
+
* @param config - Configuration object
|
|
73
|
+
* @returns Effect Layer for OpenTelemetry
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* import { createNodeOtelLayer, createEndpoints } from "@elto/telemetry/node";
|
|
78
|
+
*
|
|
79
|
+
* const OtelLive = createNodeOtelLayer({
|
|
80
|
+
* resource: {
|
|
81
|
+
* serviceName: "api-server",
|
|
82
|
+
* serviceVersion: "1.0.0",
|
|
83
|
+
* },
|
|
84
|
+
* endpoints: createEndpoints("http://localhost:4318"),
|
|
85
|
+
* metricExportIntervalMs: 10000, // optional, default 10000
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare function createNodeOtelLayer(config: NodeSdkConfig): effect_Layer.Layer<_effect_opentelemetry_Resource.Resource, never, never>;
|
|
90
|
+
|
|
91
|
+
export { DEFAULT_OTEL_ENDPOINT, type EndpointConfig, type NodeSdkConfig, type ResourceConfig, createEndpoints, createNodeOtelLayer };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as effect_Layer from 'effect/Layer';
|
|
2
|
+
import * as _effect_opentelemetry_Resource from '@effect/opentelemetry/Resource';
|
|
3
|
+
|
|
4
|
+
interface PIIConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Set of span attribute keys whose values will be redacted.
|
|
7
|
+
*/
|
|
8
|
+
piiAttributeKeys?: Set<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Redacted value placeholder.
|
|
11
|
+
*
|
|
12
|
+
* @default "[redacted]"
|
|
13
|
+
*/
|
|
14
|
+
redactedValue?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resource configuration for OpenTelemetry
|
|
18
|
+
*/
|
|
19
|
+
interface ResourceConfig {
|
|
20
|
+
serviceName: string;
|
|
21
|
+
serviceVersion: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Endpoint configuration for OpenTelemetry exporters
|
|
25
|
+
*/
|
|
26
|
+
interface EndpointConfig {
|
|
27
|
+
traces: string;
|
|
28
|
+
logs: string;
|
|
29
|
+
metrics?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Configuration for Node.js OpenTelemetry SDK
|
|
33
|
+
*/
|
|
34
|
+
interface NodeSdkConfig {
|
|
35
|
+
resource: ResourceConfig;
|
|
36
|
+
endpoints: EndpointConfig;
|
|
37
|
+
metricExportIntervalMs?: number;
|
|
38
|
+
piiConfig?: PIIConfig;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Default OpenTelemetry collector endpoint
|
|
43
|
+
*/
|
|
44
|
+
declare const DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
45
|
+
/**
|
|
46
|
+
* Create endpoint URLs for OTLP exporters
|
|
47
|
+
*
|
|
48
|
+
* @param baseUrl - Base URL of the OpenTelemetry collector (e.g., "http://localhost:4318")
|
|
49
|
+
* @returns Endpoint configuration object
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const endpoints = createEndpoints("http://localhost:4318");
|
|
54
|
+
* // Returns: {
|
|
55
|
+
* // traces: "http://localhost:4318/v1/traces",
|
|
56
|
+
* // logs: "http://localhost:4318/v1/logs",
|
|
57
|
+
* // metrics: "http://localhost:4318/v1/metrics"
|
|
58
|
+
* // }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
declare function createEndpoints(baseUrl?: string): EndpointConfig;
|
|
62
|
+
/**
|
|
63
|
+
* Create an OpenTelemetry Layer for Node.js applications
|
|
64
|
+
*
|
|
65
|
+
* This layer configures:
|
|
66
|
+
* - OTLP HTTP exporter for traces (to Jaeger)
|
|
67
|
+
* - OTLP HTTP exporter for metrics (to Prometheus)
|
|
68
|
+
* - OTLP HTTP exporter for logs (to Loki)
|
|
69
|
+
* - Custom span processor for redaction/filtering before export
|
|
70
|
+
* - Batch processors for efficient batching
|
|
71
|
+
*
|
|
72
|
+
* @param config - Configuration object
|
|
73
|
+
* @returns Effect Layer for OpenTelemetry
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* import { createNodeOtelLayer, createEndpoints } from "@elto/telemetry/node";
|
|
78
|
+
*
|
|
79
|
+
* const OtelLive = createNodeOtelLayer({
|
|
80
|
+
* resource: {
|
|
81
|
+
* serviceName: "api-server",
|
|
82
|
+
* serviceVersion: "1.0.0",
|
|
83
|
+
* },
|
|
84
|
+
* endpoints: createEndpoints("http://localhost:4318"),
|
|
85
|
+
* metricExportIntervalMs: 10000, // optional, default 10000
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare function createNodeOtelLayer(config: NodeSdkConfig): effect_Layer.Layer<_effect_opentelemetry_Resource.Resource, never, never>;
|
|
90
|
+
|
|
91
|
+
export { DEFAULT_OTEL_ENDPOINT, type EndpointConfig, type NodeSdkConfig, type ResourceConfig, createEndpoints, createNodeOtelLayer };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// src/node/sdk.ts
|
|
2
|
+
import { NodeSdk } from "@effect/opentelemetry";
|
|
3
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
4
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
5
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
6
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
7
|
+
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
8
|
+
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
9
|
+
|
|
10
|
+
// src/common/span-processors.ts
|
|
11
|
+
var DROP_ATTRIBUTE_KEY = "telemetry.drop";
|
|
12
|
+
var REDACTED_VALUE = "[redacted]";
|
|
13
|
+
var STATEMENT_MAX_LENGTH = 12;
|
|
14
|
+
var STATEMENT_HEAD_LENGTH = 3;
|
|
15
|
+
var STATEMENT_TAIL_LENGTH = 3;
|
|
16
|
+
var PII_ATTRIBUTE_KEYS = /* @__PURE__ */ new Set(["user.email", "note.id", "profile.id"]);
|
|
17
|
+
var getAttributes = (span) => span.attributes ?? {};
|
|
18
|
+
var setAttribute = (span, key, value) => {
|
|
19
|
+
const target = span;
|
|
20
|
+
if (typeof target.setAttribute === "function") {
|
|
21
|
+
target.setAttribute(key, value);
|
|
22
|
+
}
|
|
23
|
+
if (target.attributes) {
|
|
24
|
+
target.attributes[key] = value;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var hashString = (value) => {
|
|
28
|
+
let hash = 2166136261;
|
|
29
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
30
|
+
hash ^= value.charCodeAt(index);
|
|
31
|
+
hash = Math.imul(hash, 16777619);
|
|
32
|
+
}
|
|
33
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
34
|
+
};
|
|
35
|
+
var hashAttributeValue = (value) => {
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return hashString(value.map((item) => String(item)).join("|"));
|
|
38
|
+
}
|
|
39
|
+
return hashString(String(value));
|
|
40
|
+
};
|
|
41
|
+
var redactConnectionString = (value) => value.replace(/\/\/([^@/]+)@/, "//***:***@");
|
|
42
|
+
var stripUrlQuery = (value) => value.split("?")[0] ?? value;
|
|
43
|
+
var shortenStatement = (statement) => {
|
|
44
|
+
if (statement.length <= STATEMENT_MAX_LENGTH) {
|
|
45
|
+
return statement;
|
|
46
|
+
}
|
|
47
|
+
const head = statement.slice(0, STATEMENT_HEAD_LENGTH);
|
|
48
|
+
const tail = statement.slice(-STATEMENT_TAIL_LENGTH);
|
|
49
|
+
const removed = statement.length - (STATEMENT_HEAD_LENGTH + STATEMENT_TAIL_LENGTH);
|
|
50
|
+
return `${head}...${tail} (+ ${removed} characters)`;
|
|
51
|
+
};
|
|
52
|
+
var shouldDropSpan = (span) => {
|
|
53
|
+
const attributes = getAttributes(span);
|
|
54
|
+
const drop = attributes[DROP_ATTRIBUTE_KEY];
|
|
55
|
+
return drop === true || drop === "true";
|
|
56
|
+
};
|
|
57
|
+
var sanitizeAttributes = (span, piiAttributes) => {
|
|
58
|
+
const attributes = getAttributes(span);
|
|
59
|
+
let redactions = 0;
|
|
60
|
+
let transformed = 0;
|
|
61
|
+
const piiKeys = piiAttributes ?? PII_ATTRIBUTE_KEYS;
|
|
62
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
63
|
+
if (value === void 0) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (piiKeys.has(key)) {
|
|
67
|
+
setAttribute(span, `${key}_hash`, hashAttributeValue(value));
|
|
68
|
+
setAttribute(span, key, REDACTED_VALUE);
|
|
69
|
+
redactions += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (key === "db.connection_string" && typeof value === "string") {
|
|
73
|
+
setAttribute(span, "db.connection_string_redacted", redactConnectionString(value));
|
|
74
|
+
setAttribute(span, key, REDACTED_VALUE);
|
|
75
|
+
redactions += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (key === "db.statement" && typeof value === "string") {
|
|
79
|
+
const shortened = shortenStatement(value);
|
|
80
|
+
if (shortened !== value) {
|
|
81
|
+
setAttribute(span, key, shortened);
|
|
82
|
+
transformed += 1;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (key === "http.url" && typeof value === "string") {
|
|
87
|
+
const sanitized = stripUrlQuery(value);
|
|
88
|
+
if (sanitized !== value) {
|
|
89
|
+
setAttribute(span, key, sanitized);
|
|
90
|
+
transformed += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const statusCode = attributes["http.status_code"];
|
|
95
|
+
if (typeof statusCode === "number") {
|
|
96
|
+
setAttribute(span, "http.status_category", `${Math.floor(statusCode / 100)}xx`);
|
|
97
|
+
transformed += 1;
|
|
98
|
+
}
|
|
99
|
+
if (redactions > 0 || transformed > 0) {
|
|
100
|
+
setAttribute(span, "telemetry.redactions", redactions);
|
|
101
|
+
setAttribute(span, "telemetry.transformations", transformed);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var CustomSpanProcessor = class {
|
|
105
|
+
constructor(delegate, piiAttributes) {
|
|
106
|
+
this.delegate = delegate;
|
|
107
|
+
this.piiAttributes = piiAttributes;
|
|
108
|
+
}
|
|
109
|
+
onStart(span, parentContext) {
|
|
110
|
+
sanitizeAttributes(span, this.piiAttributes);
|
|
111
|
+
this.delegate.onStart(span, parentContext);
|
|
112
|
+
}
|
|
113
|
+
onEnd(span) {
|
|
114
|
+
sanitizeAttributes(span, this.piiAttributes);
|
|
115
|
+
if (shouldDropSpan(span)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.delegate.onEnd(span);
|
|
119
|
+
}
|
|
120
|
+
shutdown() {
|
|
121
|
+
return this.delegate.shutdown();
|
|
122
|
+
}
|
|
123
|
+
forceFlush() {
|
|
124
|
+
return this.delegate.forceFlush();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var createCustomSpanProcessor = (delegate, piiConfig) => new CustomSpanProcessor(delegate, piiConfig?.piiAttributeKeys);
|
|
128
|
+
|
|
129
|
+
// src/node/sdk.ts
|
|
130
|
+
var DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
131
|
+
function createEndpoints(baseUrl = DEFAULT_OTEL_ENDPOINT) {
|
|
132
|
+
return {
|
|
133
|
+
traces: `${baseUrl}/v1/traces`,
|
|
134
|
+
logs: `${baseUrl}/v1/logs`,
|
|
135
|
+
metrics: `${baseUrl}/v1/metrics`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function createNodeOtelLayer(config) {
|
|
139
|
+
const { resource, endpoints, metricExportIntervalMs = 1e4 } = config;
|
|
140
|
+
if (!endpoints.metrics) {
|
|
141
|
+
throw new Error("metrics endpoint is required for Node.js SDK");
|
|
142
|
+
}
|
|
143
|
+
const metricsUrl = endpoints.metrics;
|
|
144
|
+
return NodeSdk.layer(() => ({
|
|
145
|
+
resource: {
|
|
146
|
+
serviceName: resource.serviceName,
|
|
147
|
+
serviceVersion: resource.serviceVersion
|
|
148
|
+
},
|
|
149
|
+
spanProcessor: createCustomSpanProcessor(
|
|
150
|
+
new BatchSpanProcessor(
|
|
151
|
+
new OTLPTraceExporter({
|
|
152
|
+
url: endpoints.traces
|
|
153
|
+
})
|
|
154
|
+
),
|
|
155
|
+
config.piiConfig
|
|
156
|
+
),
|
|
157
|
+
metricReader: new PeriodicExportingMetricReader({
|
|
158
|
+
exporter: new OTLPMetricExporter({
|
|
159
|
+
url: metricsUrl
|
|
160
|
+
}),
|
|
161
|
+
exportIntervalMillis: metricExportIntervalMs
|
|
162
|
+
}),
|
|
163
|
+
logRecordProcessor: new BatchLogRecordProcessor(
|
|
164
|
+
new OTLPLogExporter({
|
|
165
|
+
url: endpoints.logs
|
|
166
|
+
})
|
|
167
|
+
)
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
DEFAULT_OTEL_ENDPOINT,
|
|
172
|
+
createEndpoints,
|
|
173
|
+
createNodeOtelLayer
|
|
174
|
+
};
|
|
175
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/node/sdk.ts","../../src/common/span-processors.ts"],"sourcesContent":["import { NodeSdk } from \"@effect/opentelemetry\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport { PeriodicExportingMetricReader } from \"@opentelemetry/sdk-metrics\";\nimport { BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { createCustomSpanProcessor } from \"../common/span-processors.js\";\nimport type { NodeSdkConfig, EndpointConfig } from \"../common/types.js\";\n\n/**\n * Default OpenTelemetry collector endpoint\n */\nexport const DEFAULT_OTEL_ENDPOINT = \"http://localhost:4318\";\n\n/**\n * Create endpoint URLs for OTLP exporters\n *\n * @param baseUrl - Base URL of the OpenTelemetry collector (e.g., \"http://localhost:4318\")\n * @returns Endpoint configuration object\n *\n * @example\n * ```typescript\n * const endpoints = createEndpoints(\"http://localhost:4318\");\n * // Returns: {\n * // traces: \"http://localhost:4318/v1/traces\",\n * // logs: \"http://localhost:4318/v1/logs\",\n * // metrics: \"http://localhost:4318/v1/metrics\"\n * // }\n * ```\n */\nexport function createEndpoints(baseUrl: string = DEFAULT_OTEL_ENDPOINT): EndpointConfig {\n return {\n traces: `${baseUrl}/v1/traces`,\n logs: `${baseUrl}/v1/logs`,\n metrics: `${baseUrl}/v1/metrics`,\n };\n}\n\n/**\n * Create an OpenTelemetry Layer for Node.js applications\n *\n * This layer configures:\n * - OTLP HTTP exporter for traces (to Jaeger)\n * - OTLP HTTP exporter for metrics (to Prometheus)\n * - OTLP HTTP exporter for logs (to Loki)\n * - Custom span processor for redaction/filtering before export\n * - Batch processors for efficient batching\n *\n * @param config - Configuration object\n * @returns Effect Layer for OpenTelemetry\n *\n * @example\n * ```typescript\n * import { createNodeOtelLayer, createEndpoints } from \"@elto/telemetry/node\";\n *\n * const OtelLive = createNodeOtelLayer({\n * resource: {\n * serviceName: \"api-server\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: createEndpoints(\"http://localhost:4318\"),\n * metricExportIntervalMs: 10000, // optional, default 10000\n * });\n * ```\n */\nexport function createNodeOtelLayer(config: NodeSdkConfig) {\n const { resource, endpoints, metricExportIntervalMs = 10000 } = config;\n\n if (!endpoints.metrics) {\n throw new Error(\"metrics endpoint is required for Node.js SDK\");\n }\n\n const metricsUrl = endpoints.metrics;\n\n return NodeSdk.layer(() => ({\n resource: {\n serviceName: resource.serviceName,\n serviceVersion: resource.serviceVersion,\n },\n spanProcessor: createCustomSpanProcessor(\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: endpoints.traces,\n }),\n ),\n config.piiConfig,\n ),\n metricReader: new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: metricsUrl,\n }),\n exportIntervalMillis: metricExportIntervalMs,\n }),\n logRecordProcessor: new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: endpoints.logs,\n }),\n ),\n }));\n}\n","import type { AttributeValue, Context } from \"@opentelemetry/api\";\nimport type { ReadableSpan, Span, SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport type { PIIConfig } from \"./types\";\n\n/**\n * Default span processor used by @elto/telemetry to normalize and sanitize\n * attributes before export. It is intentionally small and opinionated so\n * demo traces stay safe and readable.\n */\ntype SpanAttributes = Record<string, AttributeValue | undefined>;\n\nconst DROP_ATTRIBUTE_KEY = \"telemetry.drop\";\nconst REDACTED_VALUE = \"[redacted]\";\nconst STATEMENT_MAX_LENGTH = 12;\nconst STATEMENT_HEAD_LENGTH = 3;\nconst STATEMENT_TAIL_LENGTH = 3;\n\nconst PII_ATTRIBUTE_KEYS = new Set([\"user.email\", \"note.id\", \"profile.id\"]);\n\nconst getAttributes = (span: Span | ReadableSpan): SpanAttributes =>\n (span as { attributes?: SpanAttributes }).attributes ?? {};\n\nconst setAttribute = (span: Span | ReadableSpan, key: string, value: AttributeValue) => {\n const target = span as {\n setAttribute?: (attributeKey: string, attributeValue: AttributeValue) => void;\n attributes?: SpanAttributes;\n };\n\n if (typeof target.setAttribute === \"function\") {\n target.setAttribute(key, value);\n }\n\n if (target.attributes) {\n target.attributes[key] = value;\n }\n};\n\nconst hashString = (value: string) => {\n let hash = 2166136261;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n};\n\nconst hashAttributeValue = (value: AttributeValue) => {\n if (Array.isArray(value)) {\n return hashString(value.map((item) => String(item)).join(\"|\"));\n }\n return hashString(String(value));\n};\n\nconst redactConnectionString = (value: string) => value.replace(/\\/\\/([^@/]+)@/, \"//***:***@\");\n\nconst stripUrlQuery = (value: string) => value.split(\"?\")[0] ?? value;\n\n/**\n * Shorten a long SQL statement to a leading head + tail with a marker that\n * explains how many characters were removed.\n */\nconst shortenStatement = (statement: string) => {\n if (statement.length <= STATEMENT_MAX_LENGTH) {\n return statement;\n }\n\n const head = statement.slice(0, STATEMENT_HEAD_LENGTH);\n const tail = statement.slice(-STATEMENT_TAIL_LENGTH);\n const removed = statement.length - (STATEMENT_HEAD_LENGTH + STATEMENT_TAIL_LENGTH);\n return `${head}...${tail} (+ ${removed} characters)`;\n};\n\nconst shouldDropSpan = (span: ReadableSpan) => {\n const attributes = getAttributes(span);\n const drop = attributes[DROP_ATTRIBUTE_KEY];\n return drop === true || drop === \"true\";\n};\n\nconst sanitizeAttributes = (span: Span | ReadableSpan, piiAttributes?: Set<string>) => {\n const attributes = getAttributes(span);\n let redactions = 0;\n let transformed = 0;\n\n const piiKeys = piiAttributes ?? PII_ATTRIBUTE_KEYS;\n for (const [key, value] of Object.entries(attributes)) {\n if (value === undefined) {\n continue;\n }\n\n if (piiKeys.has(key)) {\n setAttribute(span, `${key}_hash`, hashAttributeValue(value));\n setAttribute(span, key, REDACTED_VALUE);\n redactions += 1;\n continue;\n }\n\n if (key === \"db.connection_string\" && typeof value === \"string\") {\n setAttribute(span, \"db.connection_string_redacted\", redactConnectionString(value));\n setAttribute(span, key, REDACTED_VALUE);\n redactions += 1;\n continue;\n }\n\n if (key === \"db.statement\" && typeof value === \"string\") {\n const shortened = shortenStatement(value);\n if (shortened !== value) {\n setAttribute(span, key, shortened);\n transformed += 1;\n }\n continue;\n }\n\n if (key === \"http.url\" && typeof value === \"string\") {\n const sanitized = stripUrlQuery(value);\n if (sanitized !== value) {\n setAttribute(span, key, sanitized);\n transformed += 1;\n }\n }\n }\n\n const statusCode = attributes[\"http.status_code\"];\n if (typeof statusCode === \"number\") {\n setAttribute(span, \"http.status_category\", `${Math.floor(statusCode / 100)}xx`);\n transformed += 1;\n }\n\n if (redactions > 0 || transformed > 0) {\n setAttribute(span, \"telemetry.redactions\", redactions);\n setAttribute(span, \"telemetry.transformations\", transformed);\n }\n};\n\n/**\n * A SpanProcessor that applies redaction, attribute normalization, and optional\n * dropping before delegating to another processor (typically a BatchSpanProcessor).\n *\n * Behavior:\n * - Redacts `user.email`, `note.id`, and `profile.id` while adding `<key>_hash`.\n * - Redacts `db.connection_string` and stores a sanitized copy in\n * `db.connection_string_redacted`.\n * - Shortens `db.statement` when it exceeds the configured threshold.\n * - Strips query strings from `http.url`.\n * - Adds `http.status_category` when `http.status_code` is present.\n * - Adds `telemetry.redactions` and `telemetry.transformations` counters.\n * - Drops spans when `telemetry.drop` is `true` or `\"true\"`.\n *\n * @param delegate - The SpanProcessor to delegate to after sanitization\n * @param piiAttributes - Optional set of span attribute keys whose values will be redacted.\n * If provided, this overrides the default {@link PII_ATTRIBUTE_KEYS} (`user.email`, `note.id`, `profile.id`).\n * For each matching key, the original value is replaced with `[redacted]` and a hash\n * is stored in a `<key>_hash` attribute.\n */\nexport class CustomSpanProcessor implements SpanProcessor {\n constructor(\n private readonly delegate: SpanProcessor,\n private readonly piiAttributes?: Set<string>,\n ) {}\n\n onStart(span: Span, parentContext: Context): void {\n sanitizeAttributes(span, this.piiAttributes);\n this.delegate.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n sanitizeAttributes(span, this.piiAttributes);\n if (shouldDropSpan(span)) {\n return;\n }\n this.delegate.onEnd(span);\n }\n\n shutdown(): Promise<void> {\n return this.delegate.shutdown();\n }\n\n forceFlush(): Promise<void> {\n return this.delegate.forceFlush();\n }\n}\n\n/**\n * Convenience factory for wrapping a delegate SpanProcessor with the\n * @elto/telemetry custom sanitation behavior.\n */\nexport const createCustomSpanProcessor = (delegate: SpanProcessor, piiConfig?: PIIConfig) =>\n new CustomSpanProcessor(delegate, piiConfig?.piiAttributeKeys);\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,SAAS,yBAAyB;AAClC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,qCAAqC;AAC9C,SAAS,+BAA+B;;;ACKxC,IAAM,qBAAqB;AAC3B,IAAM,iBAAiB;AACvB,IAAM,uBAAuB;AAC7B,IAAM,wBAAwB;AAC9B,IAAM,wBAAwB;AAE9B,IAAM,qBAAqB,oBAAI,IAAI,CAAC,cAAc,WAAW,YAAY,CAAC;AAE1E,IAAM,gBAAgB,CAAC,SACpB,KAAyC,cAAc,CAAC;AAE3D,IAAM,eAAe,CAAC,MAA2B,KAAa,UAA0B;AACtF,QAAM,SAAS;AAKf,MAAI,OAAO,OAAO,iBAAiB,YAAY;AAC7C,WAAO,aAAa,KAAK,KAAK;AAAA,EAChC;AAEA,MAAI,OAAO,YAAY;AACrB,WAAO,WAAW,GAAG,IAAI;AAAA,EAC3B;AACF;AAEA,IAAM,aAAa,CAAC,UAAkB;AACpC,MAAI,OAAO;AACX,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,GAAG;AACpD,YAAQ,MAAM,WAAW,KAAK;AAC9B,WAAO,KAAK,KAAK,MAAM,QAAQ;AAAA,EACjC;AACA,UAAQ,SAAS,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAClD;AAEA,IAAM,qBAAqB,CAAC,UAA0B;AACpD,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,WAAW,MAAM,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,EAC/D;AACA,SAAO,WAAW,OAAO,KAAK,CAAC;AACjC;AAEA,IAAM,yBAAyB,CAAC,UAAkB,MAAM,QAAQ,iBAAiB,YAAY;AAE7F,IAAM,gBAAgB,CAAC,UAAkB,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK;AAMhE,IAAM,mBAAmB,CAAC,cAAsB;AAC9C,MAAI,UAAU,UAAU,sBAAsB;AAC5C,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,UAAU,MAAM,GAAG,qBAAqB;AACrD,QAAM,OAAO,UAAU,MAAM,CAAC,qBAAqB;AACnD,QAAM,UAAU,UAAU,UAAU,wBAAwB;AAC5D,SAAO,GAAG,IAAI,MAAM,IAAI,OAAO,OAAO;AACxC;AAEA,IAAM,iBAAiB,CAAC,SAAuB;AAC7C,QAAM,aAAa,cAAc,IAAI;AACrC,QAAM,OAAO,WAAW,kBAAkB;AAC1C,SAAO,SAAS,QAAQ,SAAS;AACnC;AAEA,IAAM,qBAAqB,CAAC,MAA2B,kBAAgC;AACrF,QAAM,aAAa,cAAc,IAAI;AACrC,MAAI,aAAa;AACjB,MAAI,cAAc;AAElB,QAAM,UAAU,iBAAiB;AACjC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACrD,QAAI,UAAU,QAAW;AACvB;AAAA,IACF;AAEA,QAAI,QAAQ,IAAI,GAAG,GAAG;AACpB,mBAAa,MAAM,GAAG,GAAG,SAAS,mBAAmB,KAAK,CAAC;AAC3D,mBAAa,MAAM,KAAK,cAAc;AACtC,oBAAc;AACd;AAAA,IACF;AAEA,QAAI,QAAQ,0BAA0B,OAAO,UAAU,UAAU;AAC/D,mBAAa,MAAM,iCAAiC,uBAAuB,KAAK,CAAC;AACjF,mBAAa,MAAM,KAAK,cAAc;AACtC,oBAAc;AACd;AAAA,IACF;AAEA,QAAI,QAAQ,kBAAkB,OAAO,UAAU,UAAU;AACvD,YAAM,YAAY,iBAAiB,KAAK;AACxC,UAAI,cAAc,OAAO;AACvB,qBAAa,MAAM,KAAK,SAAS;AACjC,uBAAe;AAAA,MACjB;AACA;AAAA,IACF;AAEA,QAAI,QAAQ,cAAc,OAAO,UAAU,UAAU;AACnD,YAAM,YAAY,cAAc,KAAK;AACrC,UAAI,cAAc,OAAO;AACvB,qBAAa,MAAM,KAAK,SAAS;AACjC,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAa,WAAW,kBAAkB;AAChD,MAAI,OAAO,eAAe,UAAU;AAClC,iBAAa,MAAM,wBAAwB,GAAG,KAAK,MAAM,aAAa,GAAG,CAAC,IAAI;AAC9E,mBAAe;AAAA,EACjB;AAEA,MAAI,aAAa,KAAK,cAAc,GAAG;AACrC,iBAAa,MAAM,wBAAwB,UAAU;AACrD,iBAAa,MAAM,6BAA6B,WAAW;AAAA,EAC7D;AACF;AAsBO,IAAM,sBAAN,MAAmD;AAAA,EACxD,YACmB,UACA,eACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAEH,QAAQ,MAAY,eAA8B;AAChD,uBAAmB,MAAM,KAAK,aAAa;AAC3C,SAAK,SAAS,QAAQ,MAAM,aAAa;AAAA,EAC3C;AAAA,EAEA,MAAM,MAA0B;AAC9B,uBAAmB,MAAM,KAAK,aAAa;AAC3C,QAAI,eAAe,IAAI,GAAG;AACxB;AAAA,IACF;AACA,SAAK,SAAS,MAAM,IAAI;AAAA,EAC1B;AAAA,EAEA,WAA0B;AACxB,WAAO,KAAK,SAAS,SAAS;AAAA,EAChC;AAAA,EAEA,aAA4B;AAC1B,WAAO,KAAK,SAAS,WAAW;AAAA,EAClC;AACF;AAMO,IAAM,4BAA4B,CAAC,UAAyB,cACjE,IAAI,oBAAoB,UAAU,WAAW,gBAAgB;;;AD7KxD,IAAM,wBAAwB;AAkB9B,SAAS,gBAAgB,UAAkB,uBAAuC;AACvF,SAAO;AAAA,IACL,QAAQ,GAAG,OAAO;AAAA,IAClB,MAAM,GAAG,OAAO;AAAA,IAChB,SAAS,GAAG,OAAO;AAAA,EACrB;AACF;AA6BO,SAAS,oBAAoB,QAAuB;AACzD,QAAM,EAAE,UAAU,WAAW,yBAAyB,IAAM,IAAI;AAEhE,MAAI,CAAC,UAAU,SAAS;AACtB,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AAEA,QAAM,aAAa,UAAU;AAE7B,SAAO,QAAQ,MAAM,OAAO;AAAA,IAC1B,UAAU;AAAA,MACR,aAAa,SAAS;AAAA,MACtB,gBAAgB,SAAS;AAAA,IAC3B;AAAA,IACA,eAAe;AAAA,MACb,IAAI;AAAA,QACF,IAAI,kBAAkB;AAAA,UACpB,KAAK,UAAU;AAAA,QACjB,CAAC;AAAA,MACH;AAAA,MACA,OAAO;AAAA,IACT;AAAA,IACA,cAAc,IAAI,8BAA8B;AAAA,MAC9C,UAAU,IAAI,mBAAmB;AAAA,QAC/B,KAAK;AAAA,MACP,CAAC;AAAA,MACD,sBAAsB;AAAA,IACxB,CAAC;AAAA,IACD,oBAAoB,IAAI;AAAA,MACtB,IAAI,gBAAgB;AAAA,QAClB,KAAK,UAAU;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,EACF,EAAE;AACJ;","names":[]}
|