@elto/telemetry 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/node/index.cjs +13 -11
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +10 -1
- package/dist/node/index.d.ts +10 -1
- package/dist/node/index.js +13 -11
- package/dist/node/index.js.map +1 -1
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts +10 -0
- package/dist/web/index.d.ts +10 -0
- package/dist/web/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -24,6 +24,8 @@ interface EndpointConfig {
|
|
|
24
24
|
traces: string;
|
|
25
25
|
logs: string;
|
|
26
26
|
metrics?: string;
|
|
27
|
+
/** Optional headers to send with OTLP requests (e.g., for authentication) */
|
|
28
|
+
headers?: Record<string, string>;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* Configuration for Node.js OpenTelemetry SDK
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ interface EndpointConfig {
|
|
|
24
24
|
traces: string;
|
|
25
25
|
logs: string;
|
|
26
26
|
metrics?: string;
|
|
27
|
+
/** Optional headers to send with OTLP requests (e.g., for authentication) */
|
|
28
|
+
headers?: Record<string, string>;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* Configuration for Node.js OpenTelemetry SDK
|
package/dist/node/index.cjs
CHANGED
|
@@ -156,12 +156,16 @@ var createCustomSpanProcessor = (delegate, piiConfig) => new CustomSpanProcessor
|
|
|
156
156
|
|
|
157
157
|
// src/node/sdk.ts
|
|
158
158
|
var DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
159
|
-
function createEndpoints(baseUrl = DEFAULT_OTEL_ENDPOINT) {
|
|
160
|
-
|
|
159
|
+
function createEndpoints(baseUrl = DEFAULT_OTEL_ENDPOINT, headers) {
|
|
160
|
+
const config = {
|
|
161
161
|
traces: `${baseUrl}/v1/traces`,
|
|
162
162
|
logs: `${baseUrl}/v1/logs`,
|
|
163
163
|
metrics: `${baseUrl}/v1/metrics`
|
|
164
164
|
};
|
|
165
|
+
if (headers) {
|
|
166
|
+
config.headers = headers;
|
|
167
|
+
}
|
|
168
|
+
return config;
|
|
165
169
|
}
|
|
166
170
|
function createNodeOtelLayer(config) {
|
|
167
171
|
const { resource, endpoints, metricExportIntervalMs = 1e4 } = config;
|
|
@@ -169,6 +173,10 @@ function createNodeOtelLayer(config) {
|
|
|
169
173
|
throw new Error("metrics endpoint is required for Node.js SDK");
|
|
170
174
|
}
|
|
171
175
|
const metricsUrl = endpoints.metrics;
|
|
176
|
+
const headers = endpoints.headers;
|
|
177
|
+
const traceExporterConfig = headers ? { url: endpoints.traces, headers } : { url: endpoints.traces };
|
|
178
|
+
const metricExporterConfig = headers ? { url: metricsUrl, headers } : { url: metricsUrl };
|
|
179
|
+
const logExporterConfig = headers ? { url: endpoints.logs, headers } : { url: endpoints.logs };
|
|
172
180
|
return import_opentelemetry.NodeSdk.layer(() => ({
|
|
173
181
|
resource: {
|
|
174
182
|
serviceName: resource.serviceName,
|
|
@@ -176,22 +184,16 @@ function createNodeOtelLayer(config) {
|
|
|
176
184
|
},
|
|
177
185
|
spanProcessor: createCustomSpanProcessor(
|
|
178
186
|
new import_sdk_trace_base.BatchSpanProcessor(
|
|
179
|
-
new import_exporter_trace_otlp_http.OTLPTraceExporter(
|
|
180
|
-
url: endpoints.traces
|
|
181
|
-
})
|
|
187
|
+
new import_exporter_trace_otlp_http.OTLPTraceExporter(traceExporterConfig)
|
|
182
188
|
),
|
|
183
189
|
config.piiConfig
|
|
184
190
|
),
|
|
185
191
|
metricReader: new import_sdk_metrics.PeriodicExportingMetricReader({
|
|
186
|
-
exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter(
|
|
187
|
-
url: metricsUrl
|
|
188
|
-
}),
|
|
192
|
+
exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter(metricExporterConfig),
|
|
189
193
|
exportIntervalMillis: metricExportIntervalMs
|
|
190
194
|
}),
|
|
191
195
|
logRecordProcessor: new import_sdk_logs.BatchLogRecordProcessor(
|
|
192
|
-
new import_exporter_logs_otlp_http.OTLPLogExporter(
|
|
193
|
-
url: endpoints.logs
|
|
194
|
-
})
|
|
196
|
+
new import_exporter_logs_otlp_http.OTLPLogExporter(logExporterConfig)
|
|
195
197
|
)
|
|
196
198
|
}));
|
|
197
199
|
}
|
package/dist/node/index.cjs.map
CHANGED
|
@@ -1 +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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,2BAAwB;AACxB,sCAAkC;AAClC,wCAAmC;AACnC,qCAAgC;AAChC,4BAAmC;AACnC,yBAA8C;AAC9C,sBAAwC;;;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,6BAAQ,MAAM,OAAO;AAAA,IAC1B,UAAU;AAAA,MACR,aAAa,SAAS;AAAA,MACtB,gBAAgB,SAAS;AAAA,IAC3B;AAAA,IACA,eAAe;AAAA,MACb,IAAI;AAAA,QACF,IAAI,kDAAkB;AAAA,UACpB,KAAK,UAAU;AAAA,QACjB,CAAC;AAAA,MACH;AAAA,MACA,OAAO;AAAA,IACT;AAAA,IACA,cAAc,IAAI,iDAA8B;AAAA,MAC9C,UAAU,IAAI,qDAAmB;AAAA,QAC/B,KAAK;AAAA,MACP,CAAC;AAAA,MACD,sBAAsB;AAAA,IACxB,CAAC;AAAA,IACD,oBAAoB,IAAI;AAAA,MACtB,IAAI,+CAAgB;AAAA,QAClB,KAAK,UAAU;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,EACF,EAAE;AACJ;","names":[]}
|
|
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 * @param headers - Optional headers to include with OTLP requests (e.g., for authentication)\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 * // With authentication headers for Grafana Cloud:\n * const endpoints = createEndpoints(\n * \"https://otlp-gateway-prod-eu-west-2.grafana.net/otlp\",\n * { Authorization: \"Basic <base64-credentials>\" }\n * );\n * ```\n */\nexport function createEndpoints(\n baseUrl: string = DEFAULT_OTEL_ENDPOINT,\n headers?: Record<string, string>\n): EndpointConfig {\n const config: EndpointConfig = {\n traces: `${baseUrl}/v1/traces`,\n logs: `${baseUrl}/v1/logs`,\n metrics: `${baseUrl}/v1/metrics`,\n };\n if (headers) {\n config.headers = headers;\n }\n return config;\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 const headers = endpoints.headers;\n\n // Build exporter configs, only including headers when defined\n const traceExporterConfig = headers\n ? { url: endpoints.traces, headers }\n : { url: endpoints.traces };\n const metricExporterConfig = headers\n ? { url: metricsUrl, headers }\n : { url: metricsUrl };\n const logExporterConfig = headers\n ? { url: endpoints.logs, headers }\n : { url: endpoints.logs };\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(traceExporterConfig),\n ),\n config.piiConfig,\n ),\n metricReader: new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter(metricExporterConfig),\n exportIntervalMillis: metricExportIntervalMs,\n }),\n logRecordProcessor: new BatchLogRecordProcessor(\n new OTLPLogExporter(logExporterConfig),\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":[]}
|
package/dist/node/index.d.cts
CHANGED
|
@@ -27,6 +27,8 @@ interface EndpointConfig {
|
|
|
27
27
|
traces: string;
|
|
28
28
|
logs: string;
|
|
29
29
|
metrics?: string;
|
|
30
|
+
/** Optional headers to send with OTLP requests (e.g., for authentication) */
|
|
31
|
+
headers?: Record<string, string>;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Configuration for Node.js OpenTelemetry SDK
|
|
@@ -46,6 +48,7 @@ declare const DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
|
46
48
|
* Create endpoint URLs for OTLP exporters
|
|
47
49
|
*
|
|
48
50
|
* @param baseUrl - Base URL of the OpenTelemetry collector (e.g., "http://localhost:4318")
|
|
51
|
+
* @param headers - Optional headers to include with OTLP requests (e.g., for authentication)
|
|
49
52
|
* @returns Endpoint configuration object
|
|
50
53
|
*
|
|
51
54
|
* @example
|
|
@@ -56,9 +59,15 @@ declare const DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
|
56
59
|
* // logs: "http://localhost:4318/v1/logs",
|
|
57
60
|
* // metrics: "http://localhost:4318/v1/metrics"
|
|
58
61
|
* // }
|
|
62
|
+
*
|
|
63
|
+
* // With authentication headers for Grafana Cloud:
|
|
64
|
+
* const endpoints = createEndpoints(
|
|
65
|
+
* "https://otlp-gateway-prod-eu-west-2.grafana.net/otlp",
|
|
66
|
+
* { Authorization: "Basic <base64-credentials>" }
|
|
67
|
+
* );
|
|
59
68
|
* ```
|
|
60
69
|
*/
|
|
61
|
-
declare function createEndpoints(baseUrl?: string): EndpointConfig;
|
|
70
|
+
declare function createEndpoints(baseUrl?: string, headers?: Record<string, string>): EndpointConfig;
|
|
62
71
|
/**
|
|
63
72
|
* Create an OpenTelemetry Layer for Node.js applications
|
|
64
73
|
*
|
package/dist/node/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ interface EndpointConfig {
|
|
|
27
27
|
traces: string;
|
|
28
28
|
logs: string;
|
|
29
29
|
metrics?: string;
|
|
30
|
+
/** Optional headers to send with OTLP requests (e.g., for authentication) */
|
|
31
|
+
headers?: Record<string, string>;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Configuration for Node.js OpenTelemetry SDK
|
|
@@ -46,6 +48,7 @@ declare const DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
|
46
48
|
* Create endpoint URLs for OTLP exporters
|
|
47
49
|
*
|
|
48
50
|
* @param baseUrl - Base URL of the OpenTelemetry collector (e.g., "http://localhost:4318")
|
|
51
|
+
* @param headers - Optional headers to include with OTLP requests (e.g., for authentication)
|
|
49
52
|
* @returns Endpoint configuration object
|
|
50
53
|
*
|
|
51
54
|
* @example
|
|
@@ -56,9 +59,15 @@ declare const DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
|
56
59
|
* // logs: "http://localhost:4318/v1/logs",
|
|
57
60
|
* // metrics: "http://localhost:4318/v1/metrics"
|
|
58
61
|
* // }
|
|
62
|
+
*
|
|
63
|
+
* // With authentication headers for Grafana Cloud:
|
|
64
|
+
* const endpoints = createEndpoints(
|
|
65
|
+
* "https://otlp-gateway-prod-eu-west-2.grafana.net/otlp",
|
|
66
|
+
* { Authorization: "Basic <base64-credentials>" }
|
|
67
|
+
* );
|
|
59
68
|
* ```
|
|
60
69
|
*/
|
|
61
|
-
declare function createEndpoints(baseUrl?: string): EndpointConfig;
|
|
70
|
+
declare function createEndpoints(baseUrl?: string, headers?: Record<string, string>): EndpointConfig;
|
|
62
71
|
/**
|
|
63
72
|
* Create an OpenTelemetry Layer for Node.js applications
|
|
64
73
|
*
|
package/dist/node/index.js
CHANGED
|
@@ -128,12 +128,16 @@ var createCustomSpanProcessor = (delegate, piiConfig) => new CustomSpanProcessor
|
|
|
128
128
|
|
|
129
129
|
// src/node/sdk.ts
|
|
130
130
|
var DEFAULT_OTEL_ENDPOINT = "http://localhost:4318";
|
|
131
|
-
function createEndpoints(baseUrl = DEFAULT_OTEL_ENDPOINT) {
|
|
132
|
-
|
|
131
|
+
function createEndpoints(baseUrl = DEFAULT_OTEL_ENDPOINT, headers) {
|
|
132
|
+
const config = {
|
|
133
133
|
traces: `${baseUrl}/v1/traces`,
|
|
134
134
|
logs: `${baseUrl}/v1/logs`,
|
|
135
135
|
metrics: `${baseUrl}/v1/metrics`
|
|
136
136
|
};
|
|
137
|
+
if (headers) {
|
|
138
|
+
config.headers = headers;
|
|
139
|
+
}
|
|
140
|
+
return config;
|
|
137
141
|
}
|
|
138
142
|
function createNodeOtelLayer(config) {
|
|
139
143
|
const { resource, endpoints, metricExportIntervalMs = 1e4 } = config;
|
|
@@ -141,6 +145,10 @@ function createNodeOtelLayer(config) {
|
|
|
141
145
|
throw new Error("metrics endpoint is required for Node.js SDK");
|
|
142
146
|
}
|
|
143
147
|
const metricsUrl = endpoints.metrics;
|
|
148
|
+
const headers = endpoints.headers;
|
|
149
|
+
const traceExporterConfig = headers ? { url: endpoints.traces, headers } : { url: endpoints.traces };
|
|
150
|
+
const metricExporterConfig = headers ? { url: metricsUrl, headers } : { url: metricsUrl };
|
|
151
|
+
const logExporterConfig = headers ? { url: endpoints.logs, headers } : { url: endpoints.logs };
|
|
144
152
|
return NodeSdk.layer(() => ({
|
|
145
153
|
resource: {
|
|
146
154
|
serviceName: resource.serviceName,
|
|
@@ -148,22 +156,16 @@ function createNodeOtelLayer(config) {
|
|
|
148
156
|
},
|
|
149
157
|
spanProcessor: createCustomSpanProcessor(
|
|
150
158
|
new BatchSpanProcessor(
|
|
151
|
-
new OTLPTraceExporter(
|
|
152
|
-
url: endpoints.traces
|
|
153
|
-
})
|
|
159
|
+
new OTLPTraceExporter(traceExporterConfig)
|
|
154
160
|
),
|
|
155
161
|
config.piiConfig
|
|
156
162
|
),
|
|
157
163
|
metricReader: new PeriodicExportingMetricReader({
|
|
158
|
-
exporter: new OTLPMetricExporter(
|
|
159
|
-
url: metricsUrl
|
|
160
|
-
}),
|
|
164
|
+
exporter: new OTLPMetricExporter(metricExporterConfig),
|
|
161
165
|
exportIntervalMillis: metricExportIntervalMs
|
|
162
166
|
}),
|
|
163
167
|
logRecordProcessor: new BatchLogRecordProcessor(
|
|
164
|
-
new OTLPLogExporter(
|
|
165
|
-
url: endpoints.logs
|
|
166
|
-
})
|
|
168
|
+
new OTLPLogExporter(logExporterConfig)
|
|
167
169
|
)
|
|
168
170
|
}));
|
|
169
171
|
}
|
package/dist/node/index.js.map
CHANGED
|
@@ -1 +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":"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","names":[]}
|
|
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 * @param headers - Optional headers to include with OTLP requests (e.g., for authentication)\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 * // With authentication headers for Grafana Cloud:\n * const endpoints = createEndpoints(\n * \"https://otlp-gateway-prod-eu-west-2.grafana.net/otlp\",\n * { Authorization: \"Basic <base64-credentials>\" }\n * );\n * ```\n */\nexport function createEndpoints(\n baseUrl: string = DEFAULT_OTEL_ENDPOINT,\n headers?: Record<string, string>\n): EndpointConfig {\n const config: EndpointConfig = {\n traces: `${baseUrl}/v1/traces`,\n logs: `${baseUrl}/v1/logs`,\n metrics: `${baseUrl}/v1/metrics`,\n };\n if (headers) {\n config.headers = headers;\n }\n return config;\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 const headers = endpoints.headers;\n\n // Build exporter configs, only including headers when defined\n const traceExporterConfig = headers\n ? { url: endpoints.traces, headers }\n : { url: endpoints.traces };\n const metricExporterConfig = headers\n ? { url: metricsUrl, headers }\n : { url: metricsUrl };\n const logExporterConfig = headers\n ? { url: endpoints.logs, headers }\n : { url: endpoints.logs };\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(traceExporterConfig),\n ),\n config.piiConfig,\n ),\n metricReader: new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter(metricExporterConfig),\n exportIntervalMillis: metricExportIntervalMs,\n }),\n logRecordProcessor: new BatchLogRecordProcessor(\n new OTLPLogExporter(logExporterConfig),\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":[]}
|
package/dist/web/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/web/index.ts","../../src/web/sdk.ts","../../src/common/span-processors.ts","../../src/web/logger.ts"],"sourcesContent":["/**\n * Browser OpenTelemetry integration for Effect\n *\n * This module provides utilities for configuring OpenTelemetry in browser applications.\n * It supports two modes:\n * - Direct: Sends telemetry directly to the OpenTelemetry collector\n * - Proxy: Sends telemetry through your server (hides collector from browser)\n *\n * @example\n * ```typescript\n * import { createWebOtelLayer, createModeBasedEndpoints } from \"@elto/telemetry/web\";\n *\n * const OtelLive = createWebOtelLayer({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: { traces: \"\", logs: \"\" },\n * getEndpoints: createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * ),\n * });\n * ```\n *\n * @packageDocumentation\n */\nexport {\n createWebOtelLayer,\n createDirectEndpoints,\n createProxyEndpoints,\n createModeBasedEndpoints,\n getOtelMode,\n setOtelMode,\n withOtel,\n} from \"./sdk.js\";\n\nexport { createBrowserLogger } from \"./logger.js\";\n\nexport type {\n WebSdkConfig,\n ResourceConfig,\n EndpointConfig,\n ProxyConfig,\n OtelMode,\n BrowserLoggerConfig,\n LogLevel,\n LogOptions,\n BrowserLogger,\n} from \"../common/types.js\";\n","import { WebSdk } from \"@effect/opentelemetry\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-web\";\nimport { BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { ZoneContextManager } from \"@opentelemetry/context-zone\";\nimport { Layer } from \"effect\";\nimport { createCustomSpanProcessor } from \"../common/span-processors.js\";\nimport type { WebSdkConfig, EndpointConfig, ProxyConfig, OtelMode } from \"../common/types.js\";\n\nconst OTEL_MODE_KEY = \"otel-mode\";\n\n/**\n * Get the current OpenTelemetry mode from localStorage\n * @returns The current mode (\"direct\" or \"proxy\")\n */\nexport function getOtelMode(): OtelMode {\n if (typeof localStorage === \"undefined\") {\n return \"direct\";\n }\n return (localStorage.getItem(OTEL_MODE_KEY) || \"direct\") as OtelMode;\n}\n\n/**\n * Set the OpenTelemetry mode in localStorage\n * @param mode - The mode to set\n *\n * @see {@link OtelMode}\n */\nexport function setOtelMode(mode: OtelMode): void {\n if (typeof localStorage !== \"undefined\") {\n localStorage.setItem(OTEL_MODE_KEY, mode);\n }\n}\n\n/**\n * Create endpoint URLs for direct connection to OpenTelemetry collector\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 = createDirectEndpoints(\"http://localhost:4318\");\n * // Returns: {\n * // traces: \"http://localhost:4318/v1/traces\",\n * // logs: \"http://localhost:4318/v1/logs\"\n * // }\n * ```\n */\nexport function createDirectEndpoints(baseUrl: string): EndpointConfig {\n return {\n traces: `${baseUrl}/v1/traces`,\n logs: `${baseUrl}/v1/logs`,\n };\n}\n\n/**\n * Create endpoint URLs for proxy mode (telemetry sent through server)\n *\n * @param config - Proxy configuration\n * @returns Endpoint configuration object\n *\n * @example\n * ```typescript\n * const endpoints = createProxyEndpoints({\n * serverUrl: \"https://api.example.com\",\n * tracesPath: \"/api/events\", // optional, defaults to \"/api/events\"\n * logsPath: \"/api/signals\", // optional, defaults to \"/api/signals\"\n * });\n * ```\n */\nexport function createProxyEndpoints(config: ProxyConfig): EndpointConfig {\n const { serverUrl, tracesPath = \"/api/events\", logsPath = \"/api/signals\" } = config;\n return {\n traces: `${serverUrl}${tracesPath}`,\n logs: `${serverUrl}${logsPath}`,\n };\n}\n\n/**\n * Create a function that returns endpoints based on the current mode\n *\n * @param directUrl - Base URL for direct mode\n * @param proxyConfig - Configuration for proxy mode\n * @returns A function that returns the appropriate endpoints based on the current mode\n *\n * @example\n * ```typescript\n * const getEndpoints = createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * );\n * ```\n */\nexport function createModeBasedEndpoints(\n directUrl: string,\n proxyConfig: ProxyConfig,\n): () => EndpointConfig {\n return () => {\n const mode = getOtelMode();\n return mode === \"proxy\" ? createProxyEndpoints(proxyConfig) : createDirectEndpoints(directUrl);\n };\n}\n\n/**\n * Create an OpenTelemetry Layer for browser applications\n *\n * This layer configures:\n * - OTLP HTTP exporter for traces (to Jaeger)\n * - OTLP HTTP exporter for logs (to Loki)\n * - Custom span processor for redaction/filtering before export\n * - Batch processors for efficient batching\n * - Zone context manager for async context propagation in the browser\n *\n * Effect RPC automatically propagates trace context (traceId, spanId, sampled)\n * in the RPC message payload, enabling distributed tracing without manual header injection.\n *\n * @param config - Configuration object\n * @returns Effect Layer for OpenTelemetry\n *\n * @example\n * ```typescript\n * import { createWebOtelLayer, createModeBasedEndpoints } from \"@elto/telemetry/web\";\n *\n * const OtelLive = createWebOtelLayer({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: { traces: \"\", logs: \"\" }, // fallback\n * getEndpoints: createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * ),\n * piiConfig: {\n * piiAttributeKeys: new Set([\"user.email\", \"user.phone\"]),\n * },\n * });\n * ```\n */\nexport function createWebOtelLayer(config: WebSdkConfig) {\n const { resource, endpoints, getEndpoints } = config;\n\n return WebSdk.layer(() => {\n const activeEndpoints = getEndpoints ? getEndpoints() : endpoints;\n\n return {\n resource: {\n serviceName: resource.serviceName,\n serviceVersion: resource.serviceVersion,\n },\n spanProcessor: createCustomSpanProcessor(\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: activeEndpoints.traces,\n }),\n ),\n config.piiConfig,\n ),\n logRecordProcessor: new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: activeEndpoints.logs,\n }),\n ),\n contextManager: new ZoneContextManager(),\n };\n });\n}\n\n/**\n * Combined layer for RPC client with OpenTelemetry enabled.\n * Use this layer when creating the RPC client to enable distributed tracing.\n *\n * @param otelLayer - The OpenTelemetry layer\n * @param layer - The layer to merge with OpenTelemetry\n * @returns Combined layer\n *\n * @example\n * ```typescript\n * import { withOtel } from \"@elto/telemetry/web\";\n *\n * const ClientLive = withOtel(OtelLive, RpcClientLayer);\n * ```\n */\nexport function withOtel<R, E, A>(\n otelLayer: Layer.Layer<R, E, A>,\n layer: Layer.Layer<any, any, any>,\n) {\n return Layer.provideMerge(layer, otelLayer);\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","import { logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport { LoggerProvider, BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from \"@opentelemetry/semantic-conventions\";\nimport type { BrowserLoggerConfig, LogLevel, LogOptions, BrowserLogger } from \"../common/types.js\";\n\nconst severityMap: Record<LogLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warning: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n};\n\nconst severityTextMap: Record<LogLevel, string> = {\n debug: \"DEBUG\",\n info: \"INFO\",\n warning: \"WARN\",\n error: \"ERROR\",\n};\n\n/**\n * Create a browser logger that sends logs to OpenTelemetry collector\n *\n * This logger can be used in browser contexts where Effect is not available.\n * It uses the OpenTelemetry Logs API directly for browser-to-collector logging.\n *\n * @param config - Logger configuration\n * @returns Browser logger instance\n *\n * @example\n * ```typescript\n * import { createBrowserLogger, getOtelMode } from \"@elto/telemetry/web\";\n *\n * const getEndpoint = () =>\n * getOtelMode() === \"proxy\"\n * ? \"https://api.example.com/api/signals\"\n * : \"http://localhost:4318/v1/logs\";\n *\n * const logger = createBrowserLogger({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * logsEndpoint: getEndpoint,\n * });\n *\n * logger.info(\"User logged in\", { userId: \"123\" });\n * ```\n */\nexport function createBrowserLogger(config: BrowserLoggerConfig): BrowserLogger {\n const { resource, logsEndpoint } = config;\n\n // Resolve endpoint if it's a function\n const endpoint = typeof logsEndpoint === \"function\" ? logsEndpoint() : logsEndpoint;\n\n const logExporter = new OTLPLogExporter({\n url: endpoint,\n });\n\n const logRecordProcessor = new BatchLogRecordProcessor(logExporter);\n\n // Create a dedicated logger provider for browser logs with proper resource identification\n const loggerProvider = new LoggerProvider({\n resource: resourceFromAttributes({\n [ATTR_SERVICE_NAME]: resource.serviceName,\n [ATTR_SERVICE_VERSION]: resource.serviceVersion,\n }),\n processors: [logRecordProcessor],\n });\n\n // Register as global logger provider\n logs.setGlobalLoggerProvider(loggerProvider);\n\n const otelLogger = loggerProvider.getLogger(resource.serviceName, resource.serviceVersion);\n\n /**\n * Send a log to the collector\n */\n function sendLog(options: LogOptions): void {\n const { level, message, annotations = {} } = options;\n\n otelLogger.emit({\n severityNumber: severityMap[level],\n severityText: severityTextMap[level],\n body: message,\n attributes: annotations,\n });\n }\n\n /**\n * Force flush any pending logs (useful before page unload)\n */\n async function flushLogs(): Promise<void> {\n await loggerProvider.forceFlush();\n }\n\n return {\n debug: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"debug\", message, ...(annotations && { annotations }) }),\n info: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"info\", message, ...(annotations && { annotations }) }),\n warning: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"warning\", message, ...(annotations && { annotations }) }),\n error: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"error\", message, ...(annotations && { annotations }) }),\n flush: flushLogs,\n };\n}\n"],"mappings":"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","names":["import_sdk_logs","import_exporter_logs_otlp_http"]}
|
|
1
|
+
{"version":3,"sources":["../../src/web/index.ts","../../src/web/sdk.ts","../../src/common/span-processors.ts","../../src/web/logger.ts"],"sourcesContent":["/**\n * Browser OpenTelemetry integration for Effect\n *\n * This module provides utilities for configuring OpenTelemetry in browser applications.\n * It supports two modes:\n * - Direct: Sends telemetry directly to the OpenTelemetry collector\n * - Proxy: Sends telemetry through your server (hides collector from browser)\n *\n * @example\n * ```typescript\n * import { createWebOtelLayer, createModeBasedEndpoints } from \"@elto/telemetry/web\";\n *\n * const OtelLive = createWebOtelLayer({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: { traces: \"\", logs: \"\" },\n * getEndpoints: createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * ),\n * });\n * ```\n *\n * @packageDocumentation\n */\nexport {\n createWebOtelLayer,\n createDirectEndpoints,\n createProxyEndpoints,\n createModeBasedEndpoints,\n getOtelMode,\n setOtelMode,\n withOtel,\n} from \"./sdk.js\";\n\nexport { createBrowserLogger } from \"./logger.js\";\n\nexport type {\n WebSdkConfig,\n ResourceConfig,\n EndpointConfig,\n ProxyConfig,\n OtelMode,\n BrowserLoggerConfig,\n LogLevel,\n LogOptions,\n BrowserLogger,\n} from \"../common/types.js\";\n","import { WebSdk } from \"@effect/opentelemetry\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-web\";\nimport { BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { ZoneContextManager } from \"@opentelemetry/context-zone\";\nimport { Layer } from \"effect\";\nimport { createCustomSpanProcessor } from \"../common/span-processors.js\";\nimport type { WebSdkConfig, EndpointConfig, ProxyConfig, OtelMode } from \"../common/types.js\";\n\nconst OTEL_MODE_KEY = \"otel-mode\";\n\n/**\n * Get the current OpenTelemetry mode from localStorage\n *\n * @returns The current mode (\"direct\" or \"proxy\")\n *\n * @see {@link OtelMode}\n */\nexport function getOtelMode(): OtelMode {\n if (typeof localStorage === \"undefined\") {\n return \"direct\";\n }\n return (localStorage.getItem(OTEL_MODE_KEY) || \"direct\") as OtelMode;\n}\n\n/**\n * Set the OpenTelemetry mode in localStorage\n *\n * @param mode - The mode to set\n *\n * @see {@link OtelMode}\n */\nexport function setOtelMode(mode: OtelMode): void {\n if (typeof localStorage !== \"undefined\") {\n localStorage.setItem(OTEL_MODE_KEY, mode);\n }\n}\n\n/**\n * Create endpoint URLs for direct connection to OpenTelemetry collector\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 = createDirectEndpoints(\"http://localhost:4318\");\n * // Returns: {\n * // traces: \"http://localhost:4318/v1/traces\",\n * // logs: \"http://localhost:4318/v1/logs\"\n * // }\n * ```\n */\nexport function createDirectEndpoints(baseUrl: string): EndpointConfig {\n return {\n traces: `${baseUrl}/v1/traces`,\n logs: `${baseUrl}/v1/logs`,\n };\n}\n\n/**\n * Create endpoint URLs for proxy mode (telemetry sent through server)\n *\n * @param config - Proxy configuration\n * @returns Endpoint configuration object\n *\n * @example\n * ```typescript\n * const endpoints = createProxyEndpoints({\n * serverUrl: \"https://api.example.com\",\n * tracesPath: \"/api/events\", // optional, defaults to \"/api/events\"\n * logsPath: \"/api/signals\", // optional, defaults to \"/api/signals\"\n * });\n * ```\n */\nexport function createProxyEndpoints(config: ProxyConfig): EndpointConfig {\n const { serverUrl, tracesPath = \"/api/events\", logsPath = \"/api/signals\" } = config;\n return {\n traces: `${serverUrl}${tracesPath}`,\n logs: `${serverUrl}${logsPath}`,\n };\n}\n\n/**\n * Create a function that returns endpoints based on the current mode\n *\n * @param directUrl - Base URL for direct mode\n * @param proxyConfig - Configuration for proxy mode\n * @returns A function that returns the appropriate endpoints based on the current mode\n *\n * @example\n * ```typescript\n * const getEndpoints = createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * );\n * ```\n * \n * @see {@link getOtelMode}\n * @see {@link createDirectEndpoints}\n * @see {@link createProxyEndpoints}\n */\nexport function createModeBasedEndpoints(\n directUrl: string,\n proxyConfig: ProxyConfig,\n): () => EndpointConfig {\n return () => {\n const mode = getOtelMode();\n return mode === \"proxy\" ? createProxyEndpoints(proxyConfig) : createDirectEndpoints(directUrl);\n };\n}\n\n/**\n * Create an OpenTelemetry Layer for browser applications\n *\n * This layer configures:\n * - OTLP HTTP exporter for traces (to Jaeger)\n * - OTLP HTTP exporter for logs (to Loki)\n * - Custom span processor for redaction/filtering before export\n * - Batch processors for efficient batching\n * - Zone context manager for async context propagation in the browser\n *\n * Effect RPC automatically propagates trace context (traceId, spanId, sampled)\n * in the RPC message payload, enabling distributed tracing without manual header injection.\n *\n * @param config - Configuration object\n * @returns Effect Layer for OpenTelemetry\n *\n * @example\n * ```typescript\n * import { createWebOtelLayer, createModeBasedEndpoints } from \"@elto/telemetry/web\";\n *\n * const OtelLive = createWebOtelLayer({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: { traces: \"\", logs: \"\" }, // fallback\n * getEndpoints: createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * ),\n * piiConfig: {\n * piiAttributeKeys: new Set([\"user.email\", \"user.phone\"]),\n * },\n * });\n * ```\n */\nexport function createWebOtelLayer(config: WebSdkConfig) {\n const { resource, endpoints, getEndpoints } = config;\n\n return WebSdk.layer(() => {\n const activeEndpoints = getEndpoints ? getEndpoints() : endpoints;\n\n return {\n resource: {\n serviceName: resource.serviceName,\n serviceVersion: resource.serviceVersion,\n },\n spanProcessor: createCustomSpanProcessor(\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: activeEndpoints.traces,\n }),\n ),\n config.piiConfig,\n ),\n logRecordProcessor: new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: activeEndpoints.logs,\n }),\n ),\n contextManager: new ZoneContextManager(),\n };\n });\n}\n\n/**\n * Combined layer for RPC client with OpenTelemetry enabled.\n * Use this layer when creating the RPC client to enable distributed tracing.\n *\n * @param otelLayer - The OpenTelemetry layer\n * @param layer - The layer to merge with OpenTelemetry\n * @returns Combined layer\n *\n * @example\n * ```typescript\n * import { withOtel } from \"@elto/telemetry/web\";\n *\n * const ClientLive = withOtel(OtelLive, RpcClientLayer);\n * ```\n */\nexport function withOtel<R, E, A>(\n otelLayer: Layer.Layer<R, E, A>,\n layer: Layer.Layer<any, any, any>,\n) {\n return Layer.provideMerge(layer, otelLayer);\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","import { logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport { LoggerProvider, BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from \"@opentelemetry/semantic-conventions\";\nimport type { BrowserLoggerConfig, LogLevel, LogOptions, BrowserLogger } from \"../common/types.js\";\n\nconst severityMap: Record<LogLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warning: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n};\n\nconst severityTextMap: Record<LogLevel, string> = {\n debug: \"DEBUG\",\n info: \"INFO\",\n warning: \"WARN\",\n error: \"ERROR\",\n};\n\n/**\n * Create a browser logger that sends logs to OpenTelemetry collector\n *\n * This logger can be used in browser contexts where Effect is not available.\n * It uses the OpenTelemetry Logs API directly for browser-to-collector logging.\n *\n * @param config - Logger configuration\n * @returns Browser logger instance\n *\n * @example\n * ```typescript\n * import { createBrowserLogger, getOtelMode } from \"@elto/telemetry/web\";\n *\n * const getEndpoint = () =>\n * getOtelMode() === \"proxy\"\n * ? \"https://api.example.com/api/signals\"\n * : \"http://localhost:4318/v1/logs\";\n *\n * const logger = createBrowserLogger({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * logsEndpoint: getEndpoint,\n * });\n *\n * logger.info(\"User logged in\", { userId: \"123\" });\n * ```\n */\nexport function createBrowserLogger(config: BrowserLoggerConfig): BrowserLogger {\n const { resource, logsEndpoint } = config;\n\n // Resolve endpoint if it's a function\n const endpoint = typeof logsEndpoint === \"function\" ? logsEndpoint() : logsEndpoint;\n\n const logExporter = new OTLPLogExporter({\n url: endpoint,\n });\n\n const logRecordProcessor = new BatchLogRecordProcessor(logExporter);\n\n // Create a dedicated logger provider for browser logs with proper resource identification\n const loggerProvider = new LoggerProvider({\n resource: resourceFromAttributes({\n [ATTR_SERVICE_NAME]: resource.serviceName,\n [ATTR_SERVICE_VERSION]: resource.serviceVersion,\n }),\n processors: [logRecordProcessor],\n });\n\n // Register as global logger provider\n logs.setGlobalLoggerProvider(loggerProvider);\n\n const otelLogger = loggerProvider.getLogger(resource.serviceName, resource.serviceVersion);\n\n /**\n * Send a log to the collector\n */\n function sendLog(options: LogOptions): void {\n const { level, message, annotations = {} } = options;\n\n otelLogger.emit({\n severityNumber: severityMap[level],\n severityText: severityTextMap[level],\n body: message,\n attributes: annotations,\n });\n }\n\n /**\n * Force flush any pending logs (useful before page unload)\n */\n async function flushLogs(): Promise<void> {\n await loggerProvider.forceFlush();\n }\n\n return {\n debug: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"debug\", message, ...(annotations && { annotations }) }),\n info: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"info\", message, ...(annotations && { annotations }) }),\n warning: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"warning\", message, ...(annotations && { annotations }) }),\n error: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"error\", message, ...(annotations && { annotations }) }),\n flush: flushLogs,\n };\n}\n"],"mappings":"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","names":["import_sdk_logs","import_exporter_logs_otlp_http"]}
|
package/dist/web/index.d.cts
CHANGED
|
@@ -27,6 +27,8 @@ interface EndpointConfig {
|
|
|
27
27
|
traces: string;
|
|
28
28
|
logs: string;
|
|
29
29
|
metrics?: string;
|
|
30
|
+
/** Optional headers to send with OTLP requests (e.g., for authentication) */
|
|
31
|
+
headers?: Record<string, string>;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Configuration for Web OpenTelemetry SDK
|
|
@@ -84,11 +86,15 @@ interface BrowserLogger {
|
|
|
84
86
|
|
|
85
87
|
/**
|
|
86
88
|
* Get the current OpenTelemetry mode from localStorage
|
|
89
|
+
*
|
|
87
90
|
* @returns The current mode ("direct" or "proxy")
|
|
91
|
+
*
|
|
92
|
+
* @see {@link OtelMode}
|
|
88
93
|
*/
|
|
89
94
|
declare function getOtelMode(): OtelMode;
|
|
90
95
|
/**
|
|
91
96
|
* Set the OpenTelemetry mode in localStorage
|
|
97
|
+
*
|
|
92
98
|
* @param mode - The mode to set
|
|
93
99
|
*
|
|
94
100
|
* @see {@link OtelMode}
|
|
@@ -140,6 +146,10 @@ declare function createProxyEndpoints(config: ProxyConfig): EndpointConfig;
|
|
|
140
146
|
* { serverUrl: "https://api.example.com" }
|
|
141
147
|
* );
|
|
142
148
|
* ```
|
|
149
|
+
*
|
|
150
|
+
* @see {@link getOtelMode}
|
|
151
|
+
* @see {@link createDirectEndpoints}
|
|
152
|
+
* @see {@link createProxyEndpoints}
|
|
143
153
|
*/
|
|
144
154
|
declare function createModeBasedEndpoints(directUrl: string, proxyConfig: ProxyConfig): () => EndpointConfig;
|
|
145
155
|
/**
|
package/dist/web/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ interface EndpointConfig {
|
|
|
27
27
|
traces: string;
|
|
28
28
|
logs: string;
|
|
29
29
|
metrics?: string;
|
|
30
|
+
/** Optional headers to send with OTLP requests (e.g., for authentication) */
|
|
31
|
+
headers?: Record<string, string>;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Configuration for Web OpenTelemetry SDK
|
|
@@ -84,11 +86,15 @@ interface BrowserLogger {
|
|
|
84
86
|
|
|
85
87
|
/**
|
|
86
88
|
* Get the current OpenTelemetry mode from localStorage
|
|
89
|
+
*
|
|
87
90
|
* @returns The current mode ("direct" or "proxy")
|
|
91
|
+
*
|
|
92
|
+
* @see {@link OtelMode}
|
|
88
93
|
*/
|
|
89
94
|
declare function getOtelMode(): OtelMode;
|
|
90
95
|
/**
|
|
91
96
|
* Set the OpenTelemetry mode in localStorage
|
|
97
|
+
*
|
|
92
98
|
* @param mode - The mode to set
|
|
93
99
|
*
|
|
94
100
|
* @see {@link OtelMode}
|
|
@@ -140,6 +146,10 @@ declare function createProxyEndpoints(config: ProxyConfig): EndpointConfig;
|
|
|
140
146
|
* { serverUrl: "https://api.example.com" }
|
|
141
147
|
* );
|
|
142
148
|
* ```
|
|
149
|
+
*
|
|
150
|
+
* @see {@link getOtelMode}
|
|
151
|
+
* @see {@link createDirectEndpoints}
|
|
152
|
+
* @see {@link createProxyEndpoints}
|
|
143
153
|
*/
|
|
144
154
|
declare function createModeBasedEndpoints(directUrl: string, proxyConfig: ProxyConfig): () => EndpointConfig;
|
|
145
155
|
/**
|
package/dist/web/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/web/sdk.ts","../../src/common/span-processors.ts","../../src/web/logger.ts"],"sourcesContent":["import { WebSdk } from \"@effect/opentelemetry\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-web\";\nimport { BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { ZoneContextManager } from \"@opentelemetry/context-zone\";\nimport { Layer } from \"effect\";\nimport { createCustomSpanProcessor } from \"../common/span-processors.js\";\nimport type { WebSdkConfig, EndpointConfig, ProxyConfig, OtelMode } from \"../common/types.js\";\n\nconst OTEL_MODE_KEY = \"otel-mode\";\n\n/**\n * Get the current OpenTelemetry mode from localStorage\n * @returns The current mode (\"direct\" or \"proxy\")\n */\nexport function getOtelMode(): OtelMode {\n if (typeof localStorage === \"undefined\") {\n return \"direct\";\n }\n return (localStorage.getItem(OTEL_MODE_KEY) || \"direct\") as OtelMode;\n}\n\n/**\n * Set the OpenTelemetry mode in localStorage\n * @param mode - The mode to set\n *\n * @see {@link OtelMode}\n */\nexport function setOtelMode(mode: OtelMode): void {\n if (typeof localStorage !== \"undefined\") {\n localStorage.setItem(OTEL_MODE_KEY, mode);\n }\n}\n\n/**\n * Create endpoint URLs for direct connection to OpenTelemetry collector\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 = createDirectEndpoints(\"http://localhost:4318\");\n * // Returns: {\n * // traces: \"http://localhost:4318/v1/traces\",\n * // logs: \"http://localhost:4318/v1/logs\"\n * // }\n * ```\n */\nexport function createDirectEndpoints(baseUrl: string): EndpointConfig {\n return {\n traces: `${baseUrl}/v1/traces`,\n logs: `${baseUrl}/v1/logs`,\n };\n}\n\n/**\n * Create endpoint URLs for proxy mode (telemetry sent through server)\n *\n * @param config - Proxy configuration\n * @returns Endpoint configuration object\n *\n * @example\n * ```typescript\n * const endpoints = createProxyEndpoints({\n * serverUrl: \"https://api.example.com\",\n * tracesPath: \"/api/events\", // optional, defaults to \"/api/events\"\n * logsPath: \"/api/signals\", // optional, defaults to \"/api/signals\"\n * });\n * ```\n */\nexport function createProxyEndpoints(config: ProxyConfig): EndpointConfig {\n const { serverUrl, tracesPath = \"/api/events\", logsPath = \"/api/signals\" } = config;\n return {\n traces: `${serverUrl}${tracesPath}`,\n logs: `${serverUrl}${logsPath}`,\n };\n}\n\n/**\n * Create a function that returns endpoints based on the current mode\n *\n * @param directUrl - Base URL for direct mode\n * @param proxyConfig - Configuration for proxy mode\n * @returns A function that returns the appropriate endpoints based on the current mode\n *\n * @example\n * ```typescript\n * const getEndpoints = createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * );\n * ```\n */\nexport function createModeBasedEndpoints(\n directUrl: string,\n proxyConfig: ProxyConfig,\n): () => EndpointConfig {\n return () => {\n const mode = getOtelMode();\n return mode === \"proxy\" ? createProxyEndpoints(proxyConfig) : createDirectEndpoints(directUrl);\n };\n}\n\n/**\n * Create an OpenTelemetry Layer for browser applications\n *\n * This layer configures:\n * - OTLP HTTP exporter for traces (to Jaeger)\n * - OTLP HTTP exporter for logs (to Loki)\n * - Custom span processor for redaction/filtering before export\n * - Batch processors for efficient batching\n * - Zone context manager for async context propagation in the browser\n *\n * Effect RPC automatically propagates trace context (traceId, spanId, sampled)\n * in the RPC message payload, enabling distributed tracing without manual header injection.\n *\n * @param config - Configuration object\n * @returns Effect Layer for OpenTelemetry\n *\n * @example\n * ```typescript\n * import { createWebOtelLayer, createModeBasedEndpoints } from \"@elto/telemetry/web\";\n *\n * const OtelLive = createWebOtelLayer({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: { traces: \"\", logs: \"\" }, // fallback\n * getEndpoints: createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * ),\n * piiConfig: {\n * piiAttributeKeys: new Set([\"user.email\", \"user.phone\"]),\n * },\n * });\n * ```\n */\nexport function createWebOtelLayer(config: WebSdkConfig) {\n const { resource, endpoints, getEndpoints } = config;\n\n return WebSdk.layer(() => {\n const activeEndpoints = getEndpoints ? getEndpoints() : endpoints;\n\n return {\n resource: {\n serviceName: resource.serviceName,\n serviceVersion: resource.serviceVersion,\n },\n spanProcessor: createCustomSpanProcessor(\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: activeEndpoints.traces,\n }),\n ),\n config.piiConfig,\n ),\n logRecordProcessor: new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: activeEndpoints.logs,\n }),\n ),\n contextManager: new ZoneContextManager(),\n };\n });\n}\n\n/**\n * Combined layer for RPC client with OpenTelemetry enabled.\n * Use this layer when creating the RPC client to enable distributed tracing.\n *\n * @param otelLayer - The OpenTelemetry layer\n * @param layer - The layer to merge with OpenTelemetry\n * @returns Combined layer\n *\n * @example\n * ```typescript\n * import { withOtel } from \"@elto/telemetry/web\";\n *\n * const ClientLive = withOtel(OtelLive, RpcClientLayer);\n * ```\n */\nexport function withOtel<R, E, A>(\n otelLayer: Layer.Layer<R, E, A>,\n layer: Layer.Layer<any, any, any>,\n) {\n return Layer.provideMerge(layer, otelLayer);\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","import { logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport { LoggerProvider, BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from \"@opentelemetry/semantic-conventions\";\nimport type { BrowserLoggerConfig, LogLevel, LogOptions, BrowserLogger } from \"../common/types.js\";\n\nconst severityMap: Record<LogLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warning: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n};\n\nconst severityTextMap: Record<LogLevel, string> = {\n debug: \"DEBUG\",\n info: \"INFO\",\n warning: \"WARN\",\n error: \"ERROR\",\n};\n\n/**\n * Create a browser logger that sends logs to OpenTelemetry collector\n *\n * This logger can be used in browser contexts where Effect is not available.\n * It uses the OpenTelemetry Logs API directly for browser-to-collector logging.\n *\n * @param config - Logger configuration\n * @returns Browser logger instance\n *\n * @example\n * ```typescript\n * import { createBrowserLogger, getOtelMode } from \"@elto/telemetry/web\";\n *\n * const getEndpoint = () =>\n * getOtelMode() === \"proxy\"\n * ? \"https://api.example.com/api/signals\"\n * : \"http://localhost:4318/v1/logs\";\n *\n * const logger = createBrowserLogger({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * logsEndpoint: getEndpoint,\n * });\n *\n * logger.info(\"User logged in\", { userId: \"123\" });\n * ```\n */\nexport function createBrowserLogger(config: BrowserLoggerConfig): BrowserLogger {\n const { resource, logsEndpoint } = config;\n\n // Resolve endpoint if it's a function\n const endpoint = typeof logsEndpoint === \"function\" ? logsEndpoint() : logsEndpoint;\n\n const logExporter = new OTLPLogExporter({\n url: endpoint,\n });\n\n const logRecordProcessor = new BatchLogRecordProcessor(logExporter);\n\n // Create a dedicated logger provider for browser logs with proper resource identification\n const loggerProvider = new LoggerProvider({\n resource: resourceFromAttributes({\n [ATTR_SERVICE_NAME]: resource.serviceName,\n [ATTR_SERVICE_VERSION]: resource.serviceVersion,\n }),\n processors: [logRecordProcessor],\n });\n\n // Register as global logger provider\n logs.setGlobalLoggerProvider(loggerProvider);\n\n const otelLogger = loggerProvider.getLogger(resource.serviceName, resource.serviceVersion);\n\n /**\n * Send a log to the collector\n */\n function sendLog(options: LogOptions): void {\n const { level, message, annotations = {} } = options;\n\n otelLogger.emit({\n severityNumber: severityMap[level],\n severityText: severityTextMap[level],\n body: message,\n attributes: annotations,\n });\n }\n\n /**\n * Force flush any pending logs (useful before page unload)\n */\n async function flushLogs(): Promise<void> {\n await loggerProvider.forceFlush();\n }\n\n return {\n debug: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"debug\", message, ...(annotations && { annotations }) }),\n info: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"info\", message, ...(annotations && { annotations }) }),\n warning: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"warning\", message, ...(annotations && { annotations }) }),\n error: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"error\", message, ...(annotations && { annotations }) }),\n flush: flushLogs,\n };\n}\n"],"mappings":"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","names":["BatchLogRecordProcessor","OTLPLogExporter"]}
|
|
1
|
+
{"version":3,"sources":["../../src/web/sdk.ts","../../src/common/span-processors.ts","../../src/web/logger.ts"],"sourcesContent":["import { WebSdk } from \"@effect/opentelemetry\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-web\";\nimport { BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { ZoneContextManager } from \"@opentelemetry/context-zone\";\nimport { Layer } from \"effect\";\nimport { createCustomSpanProcessor } from \"../common/span-processors.js\";\nimport type { WebSdkConfig, EndpointConfig, ProxyConfig, OtelMode } from \"../common/types.js\";\n\nconst OTEL_MODE_KEY = \"otel-mode\";\n\n/**\n * Get the current OpenTelemetry mode from localStorage\n *\n * @returns The current mode (\"direct\" or \"proxy\")\n *\n * @see {@link OtelMode}\n */\nexport function getOtelMode(): OtelMode {\n if (typeof localStorage === \"undefined\") {\n return \"direct\";\n }\n return (localStorage.getItem(OTEL_MODE_KEY) || \"direct\") as OtelMode;\n}\n\n/**\n * Set the OpenTelemetry mode in localStorage\n *\n * @param mode - The mode to set\n *\n * @see {@link OtelMode}\n */\nexport function setOtelMode(mode: OtelMode): void {\n if (typeof localStorage !== \"undefined\") {\n localStorage.setItem(OTEL_MODE_KEY, mode);\n }\n}\n\n/**\n * Create endpoint URLs for direct connection to OpenTelemetry collector\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 = createDirectEndpoints(\"http://localhost:4318\");\n * // Returns: {\n * // traces: \"http://localhost:4318/v1/traces\",\n * // logs: \"http://localhost:4318/v1/logs\"\n * // }\n * ```\n */\nexport function createDirectEndpoints(baseUrl: string): EndpointConfig {\n return {\n traces: `${baseUrl}/v1/traces`,\n logs: `${baseUrl}/v1/logs`,\n };\n}\n\n/**\n * Create endpoint URLs for proxy mode (telemetry sent through server)\n *\n * @param config - Proxy configuration\n * @returns Endpoint configuration object\n *\n * @example\n * ```typescript\n * const endpoints = createProxyEndpoints({\n * serverUrl: \"https://api.example.com\",\n * tracesPath: \"/api/events\", // optional, defaults to \"/api/events\"\n * logsPath: \"/api/signals\", // optional, defaults to \"/api/signals\"\n * });\n * ```\n */\nexport function createProxyEndpoints(config: ProxyConfig): EndpointConfig {\n const { serverUrl, tracesPath = \"/api/events\", logsPath = \"/api/signals\" } = config;\n return {\n traces: `${serverUrl}${tracesPath}`,\n logs: `${serverUrl}${logsPath}`,\n };\n}\n\n/**\n * Create a function that returns endpoints based on the current mode\n *\n * @param directUrl - Base URL for direct mode\n * @param proxyConfig - Configuration for proxy mode\n * @returns A function that returns the appropriate endpoints based on the current mode\n *\n * @example\n * ```typescript\n * const getEndpoints = createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * );\n * ```\n * \n * @see {@link getOtelMode}\n * @see {@link createDirectEndpoints}\n * @see {@link createProxyEndpoints}\n */\nexport function createModeBasedEndpoints(\n directUrl: string,\n proxyConfig: ProxyConfig,\n): () => EndpointConfig {\n return () => {\n const mode = getOtelMode();\n return mode === \"proxy\" ? createProxyEndpoints(proxyConfig) : createDirectEndpoints(directUrl);\n };\n}\n\n/**\n * Create an OpenTelemetry Layer for browser applications\n *\n * This layer configures:\n * - OTLP HTTP exporter for traces (to Jaeger)\n * - OTLP HTTP exporter for logs (to Loki)\n * - Custom span processor for redaction/filtering before export\n * - Batch processors for efficient batching\n * - Zone context manager for async context propagation in the browser\n *\n * Effect RPC automatically propagates trace context (traceId, spanId, sampled)\n * in the RPC message payload, enabling distributed tracing without manual header injection.\n *\n * @param config - Configuration object\n * @returns Effect Layer for OpenTelemetry\n *\n * @example\n * ```typescript\n * import { createWebOtelLayer, createModeBasedEndpoints } from \"@elto/telemetry/web\";\n *\n * const OtelLive = createWebOtelLayer({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * endpoints: { traces: \"\", logs: \"\" }, // fallback\n * getEndpoints: createModeBasedEndpoints(\n * \"http://localhost:4318\",\n * { serverUrl: \"https://api.example.com\" }\n * ),\n * piiConfig: {\n * piiAttributeKeys: new Set([\"user.email\", \"user.phone\"]),\n * },\n * });\n * ```\n */\nexport function createWebOtelLayer(config: WebSdkConfig) {\n const { resource, endpoints, getEndpoints } = config;\n\n return WebSdk.layer(() => {\n const activeEndpoints = getEndpoints ? getEndpoints() : endpoints;\n\n return {\n resource: {\n serviceName: resource.serviceName,\n serviceVersion: resource.serviceVersion,\n },\n spanProcessor: createCustomSpanProcessor(\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: activeEndpoints.traces,\n }),\n ),\n config.piiConfig,\n ),\n logRecordProcessor: new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: activeEndpoints.logs,\n }),\n ),\n contextManager: new ZoneContextManager(),\n };\n });\n}\n\n/**\n * Combined layer for RPC client with OpenTelemetry enabled.\n * Use this layer when creating the RPC client to enable distributed tracing.\n *\n * @param otelLayer - The OpenTelemetry layer\n * @param layer - The layer to merge with OpenTelemetry\n * @returns Combined layer\n *\n * @example\n * ```typescript\n * import { withOtel } from \"@elto/telemetry/web\";\n *\n * const ClientLive = withOtel(OtelLive, RpcClientLayer);\n * ```\n */\nexport function withOtel<R, E, A>(\n otelLayer: Layer.Layer<R, E, A>,\n layer: Layer.Layer<any, any, any>,\n) {\n return Layer.provideMerge(layer, otelLayer);\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","import { logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport { LoggerProvider, BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from \"@opentelemetry/semantic-conventions\";\nimport type { BrowserLoggerConfig, LogLevel, LogOptions, BrowserLogger } from \"../common/types.js\";\n\nconst severityMap: Record<LogLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warning: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n};\n\nconst severityTextMap: Record<LogLevel, string> = {\n debug: \"DEBUG\",\n info: \"INFO\",\n warning: \"WARN\",\n error: \"ERROR\",\n};\n\n/**\n * Create a browser logger that sends logs to OpenTelemetry collector\n *\n * This logger can be used in browser contexts where Effect is not available.\n * It uses the OpenTelemetry Logs API directly for browser-to-collector logging.\n *\n * @param config - Logger configuration\n * @returns Browser logger instance\n *\n * @example\n * ```typescript\n * import { createBrowserLogger, getOtelMode } from \"@elto/telemetry/web\";\n *\n * const getEndpoint = () =>\n * getOtelMode() === \"proxy\"\n * ? \"https://api.example.com/api/signals\"\n * : \"http://localhost:4318/v1/logs\";\n *\n * const logger = createBrowserLogger({\n * resource: {\n * serviceName: \"web-client\",\n * serviceVersion: \"1.0.0\",\n * },\n * logsEndpoint: getEndpoint,\n * });\n *\n * logger.info(\"User logged in\", { userId: \"123\" });\n * ```\n */\nexport function createBrowserLogger(config: BrowserLoggerConfig): BrowserLogger {\n const { resource, logsEndpoint } = config;\n\n // Resolve endpoint if it's a function\n const endpoint = typeof logsEndpoint === \"function\" ? logsEndpoint() : logsEndpoint;\n\n const logExporter = new OTLPLogExporter({\n url: endpoint,\n });\n\n const logRecordProcessor = new BatchLogRecordProcessor(logExporter);\n\n // Create a dedicated logger provider for browser logs with proper resource identification\n const loggerProvider = new LoggerProvider({\n resource: resourceFromAttributes({\n [ATTR_SERVICE_NAME]: resource.serviceName,\n [ATTR_SERVICE_VERSION]: resource.serviceVersion,\n }),\n processors: [logRecordProcessor],\n });\n\n // Register as global logger provider\n logs.setGlobalLoggerProvider(loggerProvider);\n\n const otelLogger = loggerProvider.getLogger(resource.serviceName, resource.serviceVersion);\n\n /**\n * Send a log to the collector\n */\n function sendLog(options: LogOptions): void {\n const { level, message, annotations = {} } = options;\n\n otelLogger.emit({\n severityNumber: severityMap[level],\n severityText: severityTextMap[level],\n body: message,\n attributes: annotations,\n });\n }\n\n /**\n * Force flush any pending logs (useful before page unload)\n */\n async function flushLogs(): Promise<void> {\n await loggerProvider.forceFlush();\n }\n\n return {\n debug: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"debug\", message, ...(annotations && { annotations }) }),\n info: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"info\", message, ...(annotations && { annotations }) }),\n warning: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"warning\", message, ...(annotations && { annotations }) }),\n error: (message: string, annotations?: Record<string, string | number | boolean>) =>\n sendLog({ level: \"error\", message, ...(annotations && { annotations }) }),\n flush: flushLogs,\n };\n}\n"],"mappings":"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","names":["BatchLogRecordProcessor","OTLPLogExporter"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elto/telemetry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OpenTelemetry integration for Effect with Node.js and browser support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"license": "Apache-2.0",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/
|
|
17
|
+
"url": "git+https://github.com/benjamin-kraatz/effect-rpc-otel.git"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|