@safaricom-mxl/effect-sdk 0.0.11
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/README.md +39 -0
- package/dist/index.server.mjs +2 -0
- package/dist/server/index.mjs +2 -0
- package/dist/server-BsdarUpY.mjs +839 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @safaricom-mxl/effect-sdk
|
|
2
|
+
|
|
3
|
+
OpenTelemetry traces, logs, and metrics for [Effect](https://effect.website) applications, exported to the MXL OpenTelemetry collector via OTLP.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @safaricom-mxl/effect-sdk effect
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Server
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Mxl } from "@safaricom-mxl/effect-sdk/server";
|
|
15
|
+
import { Effect } from "effect";
|
|
16
|
+
|
|
17
|
+
const ObservabilityLive = Mxl.layer({ serviceName: "mxl-basket" });
|
|
18
|
+
|
|
19
|
+
const program = Effect.log("Hello!").pipe(Effect.withSpan("hello"));
|
|
20
|
+
|
|
21
|
+
Effect.runPromise(program.pipe(Effect.provide(ObservabilityLive)));
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Environment variables
|
|
25
|
+
|
|
26
|
+
| Variable | Description |
|
|
27
|
+
| ----------------------------- | ------------------------------------------ |
|
|
28
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP HTTP endpoint (preferred) |
|
|
29
|
+
| `OTLP_ENDPOINT` | Fallback OTLP endpoint |
|
|
30
|
+
| `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` export headers |
|
|
31
|
+
| `OTEL_SERVICE_NAME` | Service name |
|
|
32
|
+
| `OTEL_RESOURCE_ATTRIBUTES` | Extra resource attributes |
|
|
33
|
+
| `OTEL_SDK_DISABLED` | When `true`, installs a no-op layer |
|
|
34
|
+
|
|
35
|
+
Default endpoint: `http://127.0.0.1:4318` (local `mxl-otel-collector`).
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
MIT
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as minimumLogLevelLayer, B as layer_exports, C as withParentFromTraceparent, D as logError, E as logDebug, F as getEffectTraceCorrelation, G as normalizeOtlpBaseUrl, H as getAutoPlatformAttributes, I as parseTraceparent, J as otelGlobalContextBridgeLayer, K as buildTraceCarrierFromSpan, L as traceCorrelationLogAnnotations, M as resolveLogLevelFromEnv, N as serializeLogError, O as logInfo, P as annotateLogsWithActiveSpanCorrelation, R as shouldDisableHttpTrace, S as withParentFromTraceCarrier, T as logAtLevel, U as parseOtelResourceAttributes, V as derivePlatformAttributes, W as parseOtlpHeaders, Y as wrapTracerWithOtelContextBridge, _ as logHttpResponse, a as kafkaProducerSpanAttributes, b as spanContextFromTraceparent, c as withDependencySpan, d as jobWithSpanEffect, f as runJobWithSpan, g as logHttpRequestFailure, h as finalizeHttpSpan, i as geoSpanAttributes, j as resolveEffectLogLevelFromHttpStatus, k as logWarning, l as withPromiseDependencySpan, m as buildQueueTraceCarrierEffect, n as bufferSpanAttributes, o as queueConsumerSpanAttributes, p as buildQueueTraceCarrier, q as formatTraceparent, r as dbClientSpanAttributes, s as queueProducerSpanAttributes, t as addSpanLinksFromTraceCarriers, u as createEffectTraceEnricher, v as resolveTraceparentFromHeaders, w as buildHttpStatusLogFields, x as withHttpServerSpan, y as spanContextFromCarrier, z as tracerDisabledWhenLayer } from "./server-BsdarUpY.mjs";
|
|
2
|
+
export { layer_exports as Mxl, addSpanLinksFromTraceCarriers, annotateLogsWithActiveSpanCorrelation, bufferSpanAttributes, buildHttpStatusLogFields, buildQueueTraceCarrier, buildQueueTraceCarrierEffect, buildTraceCarrierFromSpan, createEffectTraceEnricher, dbClientSpanAttributes, derivePlatformAttributes, finalizeHttpSpan, formatTraceparent, geoSpanAttributes, getAutoPlatformAttributes, getEffectTraceCorrelation, jobWithSpanEffect, kafkaProducerSpanAttributes, logAtLevel, logDebug, logError, logHttpRequestFailure, logHttpResponse, logInfo, logWarning, minimumLogLevelLayer, normalizeOtlpBaseUrl, otelGlobalContextBridgeLayer, parseOtelResourceAttributes, parseOtlpHeaders, parseTraceparent, queueConsumerSpanAttributes, queueProducerSpanAttributes, resolveEffectLogLevelFromHttpStatus, resolveLogLevelFromEnv, resolveTraceparentFromHeaders, runJobWithSpan, serializeLogError, shouldDisableHttpTrace, spanContextFromCarrier, spanContextFromTraceparent, traceCorrelationLogAnnotations, tracerDisabledWhenLayer, withDependencySpan, withHttpServerSpan, withParentFromTraceCarrier, withParentFromTraceparent, withPromiseDependencySpan, wrapTracerWithOtelContextBridge };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as minimumLogLevelLayer, B as layer_exports, C as withParentFromTraceparent, D as logError, E as logDebug, F as getEffectTraceCorrelation, G as normalizeOtlpBaseUrl, H as getAutoPlatformAttributes, I as parseTraceparent, J as otelGlobalContextBridgeLayer, K as buildTraceCarrierFromSpan, L as traceCorrelationLogAnnotations, M as resolveLogLevelFromEnv, N as serializeLogError, O as logInfo, P as annotateLogsWithActiveSpanCorrelation, R as shouldDisableHttpTrace, S as withParentFromTraceCarrier, T as logAtLevel, U as parseOtelResourceAttributes, V as derivePlatformAttributes, W as parseOtlpHeaders, Y as wrapTracerWithOtelContextBridge, _ as logHttpResponse, a as kafkaProducerSpanAttributes, b as spanContextFromTraceparent, c as withDependencySpan, d as jobWithSpanEffect, f as runJobWithSpan, g as logHttpRequestFailure, h as finalizeHttpSpan, i as geoSpanAttributes, j as resolveEffectLogLevelFromHttpStatus, k as logWarning, l as withPromiseDependencySpan, m as buildQueueTraceCarrierEffect, n as bufferSpanAttributes, o as queueConsumerSpanAttributes, p as buildQueueTraceCarrier, q as formatTraceparent, r as dbClientSpanAttributes, s as queueProducerSpanAttributes, t as addSpanLinksFromTraceCarriers, u as createEffectTraceEnricher, v as resolveTraceparentFromHeaders, w as buildHttpStatusLogFields, x as withHttpServerSpan, y as spanContextFromCarrier, z as tracerDisabledWhenLayer } from "../server-BsdarUpY.mjs";
|
|
2
|
+
export { layer_exports as Mxl, addSpanLinksFromTraceCarriers, annotateLogsWithActiveSpanCorrelation, bufferSpanAttributes, buildHttpStatusLogFields, buildQueueTraceCarrier, buildQueueTraceCarrierEffect, buildTraceCarrierFromSpan, createEffectTraceEnricher, dbClientSpanAttributes, derivePlatformAttributes, finalizeHttpSpan, formatTraceparent, geoSpanAttributes, getAutoPlatformAttributes, getEffectTraceCorrelation, jobWithSpanEffect, kafkaProducerSpanAttributes, logAtLevel, logDebug, logError, logHttpRequestFailure, logHttpResponse, logInfo, logWarning, minimumLogLevelLayer, normalizeOtlpBaseUrl, otelGlobalContextBridgeLayer, parseOtelResourceAttributes, parseOtlpHeaders, parseTraceparent, queueConsumerSpanAttributes, queueProducerSpanAttributes, resolveEffectLogLevelFromHttpStatus, resolveLogLevelFromEnv, resolveTraceparentFromHeaders, runJobWithSpan, serializeLogError, shouldDisableHttpTrace, spanContextFromCarrier, spanContextFromTraceparent, traceCorrelationLogAnnotations, tracerDisabledWhenLayer, withDependencySpan, withHttpServerSpan, withParentFromTraceCarrier, withParentFromTraceparent, withPromiseDependencySpan, wrapTracerWithOtelContextBridge };
|
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { Config, Effect, Layer, Match, Option, References } from "effect";
|
|
2
|
+
import * as Tracer from "effect/Tracer";
|
|
3
|
+
import { FetchHttpClient, HttpMiddleware } from "effect/unstable/http";
|
|
4
|
+
import { Otlp } from "effect/unstable/observability";
|
|
5
|
+
import * as Otel from "@opentelemetry/api";
|
|
6
|
+
import { SpanKind, SpanStatusCode, TraceFlags, context, propagation, trace } from "@opentelemetry/api";
|
|
7
|
+
import { v7 } from "uuid";
|
|
8
|
+
import { platform, provider, runtime } from "std-env";
|
|
9
|
+
import * as Option$1 from "effect/Option";
|
|
10
|
+
import { OtelTracer } from "@effect/opentelemetry";
|
|
11
|
+
//#region \0rolldown/runtime.js
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __exportAll = (all, no_symbols) => {
|
|
14
|
+
let target = {};
|
|
15
|
+
for (var name in all) __defProp(target, name, {
|
|
16
|
+
get: all[name],
|
|
17
|
+
enumerable: true
|
|
18
|
+
});
|
|
19
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
20
|
+
return target;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/server/otel-context-bridge.ts
|
|
24
|
+
const OTEL_SPAN_TYPE_ID = "~@effect/opentelemetry/OtelTracer/OtelSpan";
|
|
25
|
+
const evaluate = "~effect/Effect/evaluate";
|
|
26
|
+
function formatTraceparent(span) {
|
|
27
|
+
const flags = span.sampled ? "01" : "00";
|
|
28
|
+
return `00-${span.traceId}-${span.spanId}-${flags}`;
|
|
29
|
+
}
|
|
30
|
+
function buildTraceCarrierFromSpan(span, headers) {
|
|
31
|
+
const carrier = { traceparent: formatTraceparent(span) };
|
|
32
|
+
if (headers?.tracestate) carrier.tracestate = headers.tracestate;
|
|
33
|
+
if (headers?.baggage) carrier.baggage = headers.baggage;
|
|
34
|
+
return carrier;
|
|
35
|
+
}
|
|
36
|
+
/** Map an Effect span to an OpenTelemetry context with the span active. */
|
|
37
|
+
function effectSpanToOtelContext(activeContext, span) {
|
|
38
|
+
if (typeof span === "object" && span !== null && OTEL_SPAN_TYPE_ID in span && "span" in span) return Otel.trace.setSpan(activeContext, span.span);
|
|
39
|
+
return Otel.trace.setSpanContext(activeContext, {
|
|
40
|
+
isRemote: span._tag === "ExternalSpan",
|
|
41
|
+
spanId: span.spanId,
|
|
42
|
+
traceFlags: span.sampled ? Otel.TraceFlags.SAMPLED : Otel.TraceFlags.NONE,
|
|
43
|
+
traceId: span.traceId
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Wrap a tracer so Effect spans activate global `@opentelemetry/api` context. */
|
|
47
|
+
function wrapTracerWithOtelContextBridge(base) {
|
|
48
|
+
const existingContext = base.context;
|
|
49
|
+
return Tracer.make({
|
|
50
|
+
span(options) {
|
|
51
|
+
return base.span(options);
|
|
52
|
+
},
|
|
53
|
+
context(primitive, fiber) {
|
|
54
|
+
if (fiber.currentSpan === void 0) {
|
|
55
|
+
if (existingContext) return existingContext(primitive, fiber);
|
|
56
|
+
return primitive[evaluate](fiber);
|
|
57
|
+
}
|
|
58
|
+
return Otel.context.with(effectSpanToOtelContext(Otel.context.active(), fiber.currentSpan), () => {
|
|
59
|
+
if (existingContext) return existingContext(primitive, fiber);
|
|
60
|
+
return primitive[evaluate](fiber);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Layer that wraps the configured Effect tracer with a global OTel context bridge.
|
|
67
|
+
*
|
|
68
|
+
* Must be merged after `Otlp.layerJson` so queue propagation, log correlation,
|
|
69
|
+
* and outbound W3C headers see the active server span.
|
|
70
|
+
*/
|
|
71
|
+
const otelGlobalContextBridgeLayer = Layer.effect(Tracer.Tracer, Effect.gen(function* () {
|
|
72
|
+
return wrapTracerWithOtelContextBridge(yield* Tracer.Tracer);
|
|
73
|
+
}));
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/server/otlp.ts
|
|
76
|
+
function normalizeOtlpBaseUrl(url) {
|
|
77
|
+
const trimmed = url.replace(/\/$/, "");
|
|
78
|
+
if (trimmed.endsWith("/v1/traces") || trimmed.endsWith("/v1/logs")) return trimmed.replace(/\/v1\/(traces|logs|metrics)$/, "");
|
|
79
|
+
return trimmed;
|
|
80
|
+
}
|
|
81
|
+
/** Resolve the OTLP exporter endpoint from standard env vars. */
|
|
82
|
+
const endpoint = Effect.gen(function* () {
|
|
83
|
+
const otlp = yield* Config.option(Config.string("OTEL_EXPORTER_OTLP_ENDPOINT"));
|
|
84
|
+
if (Option.isSome(otlp)) return otlp;
|
|
85
|
+
return yield* Config.option(Config.string("OTLP_ENDPOINT"));
|
|
86
|
+
});
|
|
87
|
+
const sdkDisabled = Config.boolean("OTEL_SDK_DISABLED").pipe(Config.withDefault(false));
|
|
88
|
+
/**
|
|
89
|
+
* Resolve service version / commit SHA from platform-specific env vars.
|
|
90
|
+
*
|
|
91
|
+
* Priority: npm_package_version > COMMIT_SHA > RAILWAY_GIT_COMMIT_SHA >
|
|
92
|
+
* VERCEL_GIT_COMMIT_SHA > CF_PAGES_COMMIT_SHA > RENDER_GIT_COMMIT
|
|
93
|
+
*/
|
|
94
|
+
const serviceVersion = Config.option(Config.string("npm_package_version").pipe(Config.orElse(() => Config.string("COMMIT_SHA")), Config.orElse(() => Config.string("RAILWAY_GIT_COMMIT_SHA")), Config.orElse(() => Config.string("VERCEL_GIT_COMMIT_SHA")), Config.orElse(() => Config.string("CF_PAGES_COMMIT_SHA")), Config.orElse(() => Config.string("RENDER_GIT_COMMIT"))));
|
|
95
|
+
/**
|
|
96
|
+
* Resolve deployment environment from env vars.
|
|
97
|
+
*
|
|
98
|
+
* Priority: NODE_ENV > DEPLOYMENT_ENV > "development"
|
|
99
|
+
*/
|
|
100
|
+
const environment = Config.string("NODE_ENV").pipe(Config.orElse(() => Config.string("DEPLOYMENT_ENV")), Config.withDefault("development"));
|
|
101
|
+
/** OTel-standard service name override. */
|
|
102
|
+
const otelServiceName = Config.option(Config.string("OTEL_SERVICE_NAME"));
|
|
103
|
+
/**
|
|
104
|
+
* Parse the OTel-standard `OTEL_RESOURCE_ATTRIBUTES` env var.
|
|
105
|
+
*
|
|
106
|
+
* Format per the OTel spec: comma-separated `key=value` pairs, values may be
|
|
107
|
+
* URL-encoded.
|
|
108
|
+
*/
|
|
109
|
+
const parseOtelResourceAttributes = (input) => {
|
|
110
|
+
const result = {};
|
|
111
|
+
for (const pair of input.split(",")) {
|
|
112
|
+
const eq = pair.indexOf("=");
|
|
113
|
+
if (eq === -1) continue;
|
|
114
|
+
const key = pair.slice(0, eq).trim();
|
|
115
|
+
if (!key) continue;
|
|
116
|
+
const raw = pair.slice(eq + 1).trim();
|
|
117
|
+
let value = raw;
|
|
118
|
+
try {
|
|
119
|
+
value = decodeURIComponent(raw);
|
|
120
|
+
} catch {}
|
|
121
|
+
result[key] = value;
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
};
|
|
125
|
+
/** Resolve and parse `OTEL_RESOURCE_ATTRIBUTES`. */
|
|
126
|
+
const otelResourceAttributes = Effect.gen(function* () {
|
|
127
|
+
const raw = yield* Config.option(Config.string("OTEL_RESOURCE_ATTRIBUTES"));
|
|
128
|
+
return Option.match(raw, {
|
|
129
|
+
onNone: () => ({}),
|
|
130
|
+
onSome: (value) => parseOtelResourceAttributes(value)
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* Parse `OTEL_EXPORTER_OTLP_HEADERS` (`key=value,key2=value2`) into a header map.
|
|
135
|
+
*/
|
|
136
|
+
const parseOtlpHeaders = (input) => {
|
|
137
|
+
const result = {};
|
|
138
|
+
for (const pair of input.split(",")) {
|
|
139
|
+
const eq = pair.indexOf("=");
|
|
140
|
+
if (eq === -1) continue;
|
|
141
|
+
const key = pair.slice(0, eq).trim();
|
|
142
|
+
if (!key) continue;
|
|
143
|
+
const raw = pair.slice(eq + 1).trim();
|
|
144
|
+
try {
|
|
145
|
+
result[key] = decodeURIComponent(raw);
|
|
146
|
+
} catch {
|
|
147
|
+
result[key] = raw;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
};
|
|
152
|
+
/** Resolve OTLP export headers from env. */
|
|
153
|
+
const otlpHeaders = Effect.gen(function* () {
|
|
154
|
+
const raw = yield* Config.option(Config.string("OTEL_EXPORTER_OTLP_HEADERS"));
|
|
155
|
+
return Option.match(raw, {
|
|
156
|
+
onNone: () => ({}),
|
|
157
|
+
onSome: (value) => parseOtlpHeaders(value)
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/server/platform.ts
|
|
162
|
+
const empty = {};
|
|
163
|
+
const runtimeAttrs = (rt) => Match.value(rt).pipe(Match.when("node", () => ({ "process.runtime.name": "nodejs" })), Match.whenOr("bun", "deno", "workerd", "edge-light", "fastly", (name) => ({ "process.runtime.name": name })), Match.orElse(() => empty));
|
|
164
|
+
const osAttrs = (plat) => Match.value(plat).pipe(Match.when("win32", () => ({ "os.type": "windows" })), Match.whenOr("darwin", "linux", "freebsd", "openbsd", "netbsd", (p) => ({ "os.type": p })), Match.orElse(() => empty));
|
|
165
|
+
const archAttrs = (a) => a === "" ? empty : { "host.arch": Match.value(a).pipe(Match.when("x64", () => "amd64"), Match.when("ia32", () => "x86"), Match.orElse((other) => other)) };
|
|
166
|
+
const lambdaAttrs = (env) => ({
|
|
167
|
+
"cloud.provider": "aws",
|
|
168
|
+
"cloud.platform": "aws_lambda",
|
|
169
|
+
"faas.name": env.AWS_LAMBDA_FUNCTION_NAME,
|
|
170
|
+
...env.AWS_LAMBDA_FUNCTION_VERSION && { "faas.version": env.AWS_LAMBDA_FUNCTION_VERSION },
|
|
171
|
+
...env.AWS_LAMBDA_LOG_STREAM_NAME && { "faas.instance": env.AWS_LAMBDA_LOG_STREAM_NAME },
|
|
172
|
+
...env.AWS_REGION ? { "cloud.region": env.AWS_REGION } : env.AWS_DEFAULT_REGION ? { "cloud.region": env.AWS_DEFAULT_REGION } : {}
|
|
173
|
+
});
|
|
174
|
+
const providerAttrs = (prov, env) => Match.value(prov).pipe(Match.when("cloudflare_workers", () => ({
|
|
175
|
+
"cloud.provider": "cloudflare",
|
|
176
|
+
"cloud.platform": "cloudflare.workers"
|
|
177
|
+
})), Match.when("cloudflare_pages", () => ({
|
|
178
|
+
"cloud.provider": "cloudflare",
|
|
179
|
+
"cloud.platform": "cloudflare.pages"
|
|
180
|
+
})), Match.when("vercel", () => ({
|
|
181
|
+
"cloud.provider": "vercel",
|
|
182
|
+
"cloud.platform": "vercel",
|
|
183
|
+
...env.VERCEL_REGION && { "cloud.region": env.VERCEL_REGION },
|
|
184
|
+
...env.VERCEL_DEPLOYMENT_ID && { "faas.instance": env.VERCEL_DEPLOYMENT_ID }
|
|
185
|
+
})), Match.when("netlify", () => ({
|
|
186
|
+
"cloud.provider": "netlify",
|
|
187
|
+
"cloud.platform": "netlify"
|
|
188
|
+
})), Match.whenOr("google_cloudrun", "google_cloudrun_job", () => ({
|
|
189
|
+
"cloud.provider": "gcp",
|
|
190
|
+
"cloud.platform": "gcp_cloud_run",
|
|
191
|
+
...env.K_SERVICE && { "faas.name": env.K_SERVICE },
|
|
192
|
+
...env.K_REVISION && { "faas.version": env.K_REVISION },
|
|
193
|
+
...env.CLOUD_RUN_REGION && { "cloud.region": env.CLOUD_RUN_REGION }
|
|
194
|
+
})), Match.when("firebase_app_hosting", () => ({
|
|
195
|
+
"cloud.provider": "gcp",
|
|
196
|
+
"cloud.platform": "gcp_firebase_app_hosting"
|
|
197
|
+
})), Match.when("aws_amplify", () => ({
|
|
198
|
+
"cloud.provider": "aws",
|
|
199
|
+
"cloud.platform": "aws_amplify"
|
|
200
|
+
})), Match.when("deno-deploy", () => ({
|
|
201
|
+
"cloud.provider": "deno",
|
|
202
|
+
"cloud.platform": "deno_deploy",
|
|
203
|
+
...env.DENO_REGION && { "cloud.region": env.DENO_REGION },
|
|
204
|
+
...env.DENO_DEPLOYMENT_ID && { "faas.instance": env.DENO_DEPLOYMENT_ID }
|
|
205
|
+
})), Match.when("render", () => ({
|
|
206
|
+
"cloud.provider": "render",
|
|
207
|
+
"cloud.platform": "render",
|
|
208
|
+
...env.RENDER_INSTANCE_ID && { "faas.instance": env.RENDER_INSTANCE_ID }
|
|
209
|
+
})), Match.when("railway", () => ({
|
|
210
|
+
"cloud.provider": "railway",
|
|
211
|
+
"cloud.platform": "railway",
|
|
212
|
+
...env.RAILWAY_REPLICA_ID && { "faas.instance": env.RAILWAY_REPLICA_ID }
|
|
213
|
+
})), Match.when("edgeone_pages", () => ({
|
|
214
|
+
"cloud.provider": "tencent_cloud",
|
|
215
|
+
"cloud.platform": "tencent_edgeone_pages"
|
|
216
|
+
})), Match.orElse(() => empty));
|
|
217
|
+
const workerdFallback = (rt, alreadyResolved) => rt === "workerd" && !alreadyResolved ? {
|
|
218
|
+
"cloud.provider": "cloudflare",
|
|
219
|
+
"cloud.platform": "cloudflare.workers"
|
|
220
|
+
} : empty;
|
|
221
|
+
const vercelEdgeFallback = (rt, alreadyResolved, env) => rt === "edge-light" && !alreadyResolved && env.VERCEL ? {
|
|
222
|
+
"cloud.provider": "vercel",
|
|
223
|
+
"cloud.platform": "vercel"
|
|
224
|
+
} : empty;
|
|
225
|
+
const derivePlatformAttributes = (inputs) => {
|
|
226
|
+
const { runtime: rt, provider: prov, platform: plat, arch, env } = inputs;
|
|
227
|
+
const cloudAttrs = env.AWS_LAMBDA_FUNCTION_NAME ? lambdaAttrs(env) : providerAttrs(prov, env);
|
|
228
|
+
const cloudResolved = "cloud.provider" in cloudAttrs;
|
|
229
|
+
return {
|
|
230
|
+
...rt && { "mxl.runtime": rt },
|
|
231
|
+
...prov && { "mxl.provider": prov },
|
|
232
|
+
...runtimeAttrs(rt),
|
|
233
|
+
...osAttrs(plat),
|
|
234
|
+
...archAttrs(arch),
|
|
235
|
+
...cloudAttrs,
|
|
236
|
+
...workerdFallback(rt, cloudResolved),
|
|
237
|
+
...vercelEdgeFallback(rt, cloudResolved, env)
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
const PLATFORM_ENV_KEYS = [
|
|
241
|
+
"AWS_LAMBDA_FUNCTION_NAME",
|
|
242
|
+
"AWS_LAMBDA_FUNCTION_VERSION",
|
|
243
|
+
"AWS_LAMBDA_LOG_STREAM_NAME",
|
|
244
|
+
"AWS_REGION",
|
|
245
|
+
"AWS_DEFAULT_REGION",
|
|
246
|
+
"VERCEL",
|
|
247
|
+
"VERCEL_REGION",
|
|
248
|
+
"VERCEL_DEPLOYMENT_ID",
|
|
249
|
+
"K_SERVICE",
|
|
250
|
+
"K_REVISION",
|
|
251
|
+
"CLOUD_RUN_REGION",
|
|
252
|
+
"DENO_REGION",
|
|
253
|
+
"DENO_DEPLOYMENT_ID",
|
|
254
|
+
"RENDER_INSTANCE_ID",
|
|
255
|
+
"RAILWAY_REPLICA_ID"
|
|
256
|
+
];
|
|
257
|
+
const getAutoPlatformAttributes = () => {
|
|
258
|
+
const proc = globalThis.process;
|
|
259
|
+
const procEnv = proc?.env ?? {};
|
|
260
|
+
const env = {};
|
|
261
|
+
for (const key of PLATFORM_ENV_KEYS) env[key] = procEnv[key];
|
|
262
|
+
return derivePlatformAttributes({
|
|
263
|
+
runtime: runtime ?? "",
|
|
264
|
+
provider: provider ?? "",
|
|
265
|
+
platform: platform ?? "",
|
|
266
|
+
arch: proc?.arch ?? "",
|
|
267
|
+
env
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/server/resource.ts
|
|
272
|
+
const stringOrUndefined = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
273
|
+
const DEFAULT_VCS_REPOSITORY_URL = "https://gitlab.safaricom.co.ke/mxl/apm/mxl-node-sdk";
|
|
274
|
+
function resolveCommitRevision() {
|
|
275
|
+
return stringOrUndefined(process.env.COMMIT_SHA) ?? stringOrUndefined(process.env.VERCEL_GIT_COMMIT_SHA) ?? stringOrUndefined(process.env.GITHUB_SHA) ?? stringOrUndefined(process.env.RAILWAY_GIT_COMMIT_SHA) ?? stringOrUndefined(process.env.CF_PAGES_COMMIT_SHA) ?? stringOrUndefined(process.env.RENDER_GIT_COMMIT);
|
|
276
|
+
}
|
|
277
|
+
let serviceInstanceId;
|
|
278
|
+
const getServiceInstanceId = () => serviceInstanceId ??= v7();
|
|
279
|
+
const resolveResource = (config) => Effect.gen(function* () {
|
|
280
|
+
if (yield* sdkDisabled) return {
|
|
281
|
+
endpoint: void 0,
|
|
282
|
+
environment: void 0,
|
|
283
|
+
headers: void 0,
|
|
284
|
+
resource: {
|
|
285
|
+
attributes: {},
|
|
286
|
+
serviceName: config.serviceName ?? "unknown",
|
|
287
|
+
serviceVersion: void 0
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const envEndpoint = yield* endpoint;
|
|
291
|
+
const endpoint$1 = config.endpoint ?? Option.getOrUndefined(envEndpoint) ?? "http://127.0.0.1:4318";
|
|
292
|
+
const envHeaders = yield* otlpHeaders;
|
|
293
|
+
const headers = config.headers && Object.keys(config.headers).length > 0 ? config.headers : Object.keys(envHeaders).length > 0 ? envHeaders : void 0;
|
|
294
|
+
const envServiceVersion = yield* serviceVersion;
|
|
295
|
+
const serviceVersion$1 = config.serviceVersion ?? Option.getOrUndefined(envServiceVersion);
|
|
296
|
+
const envEnvironment = yield* environment;
|
|
297
|
+
const environment$1 = config.environment ?? envEnvironment;
|
|
298
|
+
const envOtelServiceName = yield* otelServiceName;
|
|
299
|
+
const serviceName = config.serviceName ?? Option.getOrUndefined(envOtelServiceName) ?? "unknown";
|
|
300
|
+
const envResourceAttributes = yield* otelResourceAttributes;
|
|
301
|
+
const attributes = {};
|
|
302
|
+
Object.assign(attributes, getAutoPlatformAttributes());
|
|
303
|
+
attributes["mxl.sdk.type"] = config.sdkType ?? "server";
|
|
304
|
+
attributes["service.instance.id"] = getServiceInstanceId();
|
|
305
|
+
if (environment$1) {
|
|
306
|
+
attributes["deployment.environment"] = environment$1;
|
|
307
|
+
attributes["deployment.environment.name"] = environment$1;
|
|
308
|
+
}
|
|
309
|
+
if (serviceVersion$1) attributes["service.version"] = serviceVersion$1;
|
|
310
|
+
const commitRevision = resolveCommitRevision();
|
|
311
|
+
if (commitRevision) {
|
|
312
|
+
attributes["deployment.commit_sha"] = commitRevision;
|
|
313
|
+
attributes["vcs.ref.head.revision"] = commitRevision;
|
|
314
|
+
}
|
|
315
|
+
attributes["vcs.repository.url.full"] = stringOrUndefined(process.env.MXL_VCS_REPOSITORY_URL) ?? DEFAULT_VCS_REPOSITORY_URL;
|
|
316
|
+
Object.assign(attributes, envResourceAttributes);
|
|
317
|
+
if (config.attributes) Object.assign(attributes, config.attributes);
|
|
318
|
+
return {
|
|
319
|
+
endpoint: endpoint$1,
|
|
320
|
+
environment: environment$1,
|
|
321
|
+
headers,
|
|
322
|
+
resource: {
|
|
323
|
+
serviceName,
|
|
324
|
+
serviceVersion: serviceVersion$1,
|
|
325
|
+
attributes
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}).pipe(Effect.orDie);
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/server/layer.ts
|
|
331
|
+
var layer_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
|
|
332
|
+
/**
|
|
333
|
+
* Create an Effect Layer that provides OpenTelemetry traces, logs, and metrics
|
|
334
|
+
* configured for MXL's OTLP collector.
|
|
335
|
+
*
|
|
336
|
+
* Returns a no-op layer when `OTEL_SDK_DISABLED=true`.
|
|
337
|
+
*/
|
|
338
|
+
const layer = (config = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
339
|
+
const resolved = yield* resolveResource({
|
|
340
|
+
...config,
|
|
341
|
+
sdkType: "server"
|
|
342
|
+
});
|
|
343
|
+
if (!resolved.endpoint) return Layer.empty;
|
|
344
|
+
return Otlp.layerJson({
|
|
345
|
+
baseUrl: normalizeOtlpBaseUrl(resolved.endpoint),
|
|
346
|
+
headers: resolved.headers,
|
|
347
|
+
resource: resolved.resource,
|
|
348
|
+
maxBatchSize: config.maxBatchSize,
|
|
349
|
+
loggerExportInterval: config.loggerExportInterval,
|
|
350
|
+
metricsExportInterval: config.metricsExportInterval,
|
|
351
|
+
tracerExportInterval: config.tracerExportInterval,
|
|
352
|
+
shutdownTimeout: config.shutdownTimeout
|
|
353
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
354
|
+
})).pipe(Layer.updateService(Tracer.Tracer, wrapTracerWithOtelContextBridge));
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/server/http-middleware.ts
|
|
357
|
+
function shouldDisableHttpTrace(request) {
|
|
358
|
+
return request.method === "OPTIONS" || request.url === "/health" || request.url === "/healthcheck" || request.url.startsWith("/healthz") || request.url === "/metrics" || /\.(png|ico|jpg|jpeg|gif|css|js|svg|webp|woff2?)(\?.*)?$/i.test(request.url);
|
|
359
|
+
}
|
|
360
|
+
function tracerDisabledWhenLayer(filter = shouldDisableHttpTrace) {
|
|
361
|
+
return Layer.succeed(HttpMiddleware.TracerDisabledWhen, filter);
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/server/trace-correlation.ts
|
|
365
|
+
const ZERO_TRACE_ID = "00000000000000000000000000000000";
|
|
366
|
+
const ZERO_SPAN_ID = "0000000000000000";
|
|
367
|
+
/** Parse W3C `traceparent` (`00-{traceId}-{spanId}-{flags}`). */
|
|
368
|
+
function parseTraceparent(traceparent) {
|
|
369
|
+
if (!traceparent) return {};
|
|
370
|
+
const parts = traceparent.trim().split("-");
|
|
371
|
+
if (parts.length < 4) return {};
|
|
372
|
+
const traceId = parts[1];
|
|
373
|
+
const spanId = parts[2];
|
|
374
|
+
if (!traceId || !spanId) return {};
|
|
375
|
+
if (traceId.length !== 32 || spanId.length !== 16) return {};
|
|
376
|
+
if (traceId === ZERO_TRACE_ID || spanId === ZERO_SPAN_ID) return {};
|
|
377
|
+
return {
|
|
378
|
+
spanId,
|
|
379
|
+
traceId
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Resolve trace/span IDs for log correlation.
|
|
384
|
+
*
|
|
385
|
+
* Prefers the active OpenTelemetry span (when a host bridge or queue propagation
|
|
386
|
+
* is active), then falls back to a W3C `traceparent` carrier.
|
|
387
|
+
*/
|
|
388
|
+
function getEffectTraceCorrelation(options) {
|
|
389
|
+
const activeSpan = trace.getSpan(context.active());
|
|
390
|
+
if (activeSpan) {
|
|
391
|
+
const spanContext = activeSpan.spanContext();
|
|
392
|
+
if (spanContext.traceId && spanContext.traceId !== ZERO_TRACE_ID && spanContext.spanId && spanContext.spanId !== ZERO_SPAN_ID) return {
|
|
393
|
+
spanId: spanContext.spanId,
|
|
394
|
+
traceId: spanContext.traceId
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return parseTraceparent(options?.traceparent);
|
|
398
|
+
}
|
|
399
|
+
/** Build log annotation fields from active span correlation. */
|
|
400
|
+
function traceCorrelationLogAnnotations(options) {
|
|
401
|
+
const { traceparent, ...extra } = options ?? {};
|
|
402
|
+
const correlation = getEffectTraceCorrelation({ traceparent });
|
|
403
|
+
const annotations = { ...extra };
|
|
404
|
+
if (correlation.traceId) {
|
|
405
|
+
annotations["trace.trace_id"] = correlation.traceId;
|
|
406
|
+
annotations.trace_id = correlation.traceId;
|
|
407
|
+
}
|
|
408
|
+
if (correlation.spanId) {
|
|
409
|
+
annotations["trace.span_id"] = correlation.spanId;
|
|
410
|
+
annotations.span_id = correlation.spanId;
|
|
411
|
+
}
|
|
412
|
+
return annotations;
|
|
413
|
+
}
|
|
414
|
+
/** Annotate Effect logs with the active span's trace/span IDs. */
|
|
415
|
+
function annotateLogsWithActiveSpanCorrelation(program, fields) {
|
|
416
|
+
return Effect.gen(function* () {
|
|
417
|
+
const { traceparent, ...extra } = fields ?? {};
|
|
418
|
+
const annotations = { ...extra };
|
|
419
|
+
const currentSpan = yield* Effect.currentSpan.pipe(Effect.option);
|
|
420
|
+
if (Option$1.isSome(currentSpan)) {
|
|
421
|
+
annotations["trace.trace_id"] = currentSpan.value.traceId;
|
|
422
|
+
annotations.trace_id = currentSpan.value.traceId;
|
|
423
|
+
annotations["trace.span_id"] = currentSpan.value.spanId;
|
|
424
|
+
annotations.span_id = currentSpan.value.spanId;
|
|
425
|
+
} else Object.assign(annotations, traceCorrelationLogAnnotations({
|
|
426
|
+
...traceparent !== void 0 ? { traceparent } : {},
|
|
427
|
+
...extra
|
|
428
|
+
}));
|
|
429
|
+
if (Object.keys(annotations).length === 0) return yield* program;
|
|
430
|
+
return yield* program.pipe(Effect.annotateLogs(annotations));
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/server/logging.ts
|
|
435
|
+
const LOG_LEVEL_BY_ENV = {
|
|
436
|
+
trace: "Debug",
|
|
437
|
+
debug: "Debug",
|
|
438
|
+
info: "Info",
|
|
439
|
+
warn: "Warn",
|
|
440
|
+
warning: "Warn",
|
|
441
|
+
error: "Error",
|
|
442
|
+
fatal: "Fatal"
|
|
443
|
+
};
|
|
444
|
+
function resolveLogLevelFromEnv(env = process.env) {
|
|
445
|
+
const raw = env.OTEL_LOG_LEVEL ?? env.LOG_LEVEL ?? "info";
|
|
446
|
+
return LOG_LEVEL_BY_ENV[raw.trim().toLowerCase()] ?? "Info";
|
|
447
|
+
}
|
|
448
|
+
function isFiniteHttpStatus(status) {
|
|
449
|
+
return typeof status === "number" && Number.isFinite(status) && status >= 0;
|
|
450
|
+
}
|
|
451
|
+
/** Map HTTP status to Effect log level for response/failure logs. */
|
|
452
|
+
function resolveEffectLogLevelFromHttpStatus(status) {
|
|
453
|
+
if (!isFiniteHttpStatus(status) || status < 400) return "Info";
|
|
454
|
+
if (status >= 500) return "Error";
|
|
455
|
+
return "Warn";
|
|
456
|
+
}
|
|
457
|
+
/** Standard log fields for HTTP response correlation in OTEL drains. */
|
|
458
|
+
function buildHttpStatusLogFields(status) {
|
|
459
|
+
const level = resolveEffectLogLevelFromHttpStatus(status);
|
|
460
|
+
const outcome = isFiniteHttpStatus(status) && status >= 400 ? "error" : "success";
|
|
461
|
+
const logLevel = level === "Error" ? "error" : level === "Warn" ? "warn" : "info";
|
|
462
|
+
if (!isFiniteHttpStatus(status)) return {
|
|
463
|
+
"log.level": logLevel,
|
|
464
|
+
outcome
|
|
465
|
+
};
|
|
466
|
+
const statusText = String(status);
|
|
467
|
+
return {
|
|
468
|
+
"http.response.status_code": statusText,
|
|
469
|
+
"http.status": statusText,
|
|
470
|
+
"log.level": logLevel,
|
|
471
|
+
outcome
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/** Filter Effect logs below the configured minimum level. */
|
|
475
|
+
function minimumLogLevelLayer(level = resolveLogLevelFromEnv()) {
|
|
476
|
+
return Layer.succeed(References.MinimumLogLevel, level);
|
|
477
|
+
}
|
|
478
|
+
function resolveErrorMessage(error) {
|
|
479
|
+
if (error.message.trim().length > 0) return error.message;
|
|
480
|
+
if (error.cause !== void 0) {
|
|
481
|
+
if (error.cause instanceof Error && error.cause.message.trim().length > 0) return error.cause.message;
|
|
482
|
+
if (typeof error.cause === "string" && error.cause.trim().length > 0) return error.cause;
|
|
483
|
+
}
|
|
484
|
+
if ("reason" in error && typeof error.reason === "string" && error.reason.trim().length > 0) return error.reason;
|
|
485
|
+
return error.name;
|
|
486
|
+
}
|
|
487
|
+
function serializeLogError(error) {
|
|
488
|
+
if (error instanceof Error) return {
|
|
489
|
+
message: resolveErrorMessage(error),
|
|
490
|
+
name: error.name,
|
|
491
|
+
...error.stack ? { stack: error.stack } : {},
|
|
492
|
+
...error.message.trim().length === 0 && error.cause !== void 0 ? { cause: serializeLogError(error.cause) } : {}
|
|
493
|
+
};
|
|
494
|
+
return {
|
|
495
|
+
message: String(error),
|
|
496
|
+
name: "UnknownError"
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function annotateFields(fields) {
|
|
500
|
+
if (!fields || Object.keys(fields).length === 0) return (effect) => effect;
|
|
501
|
+
const serialized = Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, value instanceof Error ? serializeLogError(value) : value]));
|
|
502
|
+
return (effect) => effect.pipe(Effect.annotateLogs(serialized));
|
|
503
|
+
}
|
|
504
|
+
function logDebug(message, fields) {
|
|
505
|
+
return annotateFields(fields)(Effect.logDebug(message));
|
|
506
|
+
}
|
|
507
|
+
function logInfo(message, fields) {
|
|
508
|
+
return annotateFields(fields)(Effect.logInfo(message));
|
|
509
|
+
}
|
|
510
|
+
function logWarning(message, fields) {
|
|
511
|
+
return annotateFields(fields)(Effect.logWarning(message));
|
|
512
|
+
}
|
|
513
|
+
function logError(message, fields) {
|
|
514
|
+
return annotateFields(fields)(Effect.logError(message));
|
|
515
|
+
}
|
|
516
|
+
function logAtLevel(level, message, fields) {
|
|
517
|
+
switch (level) {
|
|
518
|
+
case "Debug": return logDebug(message, fields);
|
|
519
|
+
case "Info": return logInfo(message, fields);
|
|
520
|
+
case "Warn": return logWarning(message, fields);
|
|
521
|
+
case "Error":
|
|
522
|
+
case "Fatal": return logError(message, fields);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/server/http-tracing.ts
|
|
527
|
+
function headerValue$1(value) {
|
|
528
|
+
if (value === void 0) return void 0;
|
|
529
|
+
return typeof value === "string" ? value : value[0];
|
|
530
|
+
}
|
|
531
|
+
function spanContextFromTraceparent(traceparent) {
|
|
532
|
+
const { traceId, spanId } = parseTraceparent(traceparent);
|
|
533
|
+
if (!traceId || !spanId) return void 0;
|
|
534
|
+
return {
|
|
535
|
+
isRemote: true,
|
|
536
|
+
spanId,
|
|
537
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
538
|
+
traceId
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function resolveTraceparentFromHeaders(headers) {
|
|
542
|
+
return headerValue$1(headers.traceparent ?? headers["traceparent"]);
|
|
543
|
+
}
|
|
544
|
+
function withParentFromTraceparent(headers, program) {
|
|
545
|
+
const parentSpanContext = spanContextFromTraceparent(resolveTraceparentFromHeaders(headers));
|
|
546
|
+
if (parentSpanContext === void 0) return program;
|
|
547
|
+
return OtelTracer.withSpanContext(program, parentSpanContext);
|
|
548
|
+
}
|
|
549
|
+
function toSpanAttributes(attributes) {
|
|
550
|
+
const result = {
|
|
551
|
+
"http.request.method": attributes.method,
|
|
552
|
+
"http.route": attributes.route,
|
|
553
|
+
"url.path": attributes.route
|
|
554
|
+
};
|
|
555
|
+
if (attributes.requestId !== void 0) {
|
|
556
|
+
result["http.request_id"] = attributes.requestId;
|
|
557
|
+
result["request.id"] = attributes.requestId;
|
|
558
|
+
}
|
|
559
|
+
if (attributes.url !== void 0) result["url.full"] = attributes.url;
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
function withHttpServerSpan(name, attributes, program) {
|
|
563
|
+
return program.pipe(Effect.withSpan(name, {
|
|
564
|
+
attributes: toSpanAttributes(attributes),
|
|
565
|
+
kind: "server"
|
|
566
|
+
}), Effect.withLogSpan(name));
|
|
567
|
+
}
|
|
568
|
+
function finalizeHttpSpan(status, route) {
|
|
569
|
+
const annotations = { "http.response.status_code": status };
|
|
570
|
+
if (route !== void 0) {
|
|
571
|
+
annotations["http.route"] = route;
|
|
572
|
+
annotations["url.path"] = route;
|
|
573
|
+
}
|
|
574
|
+
return Effect.sync(() => {
|
|
575
|
+
if (status >= 500) trace.getActiveSpan()?.setStatus({
|
|
576
|
+
code: SpanStatusCode.ERROR,
|
|
577
|
+
message: String(status)
|
|
578
|
+
});
|
|
579
|
+
}).pipe(Effect.andThen(() => Effect.annotateCurrentSpan(annotations)));
|
|
580
|
+
}
|
|
581
|
+
function stringifyLogFieldValue(value) {
|
|
582
|
+
if (value === void 0) return "";
|
|
583
|
+
return String(value);
|
|
584
|
+
}
|
|
585
|
+
function buildHttpResponseLogFields(options) {
|
|
586
|
+
const fields = {
|
|
587
|
+
...buildHttpStatusLogFields(options.status),
|
|
588
|
+
...options.durationMs === void 0 ? {} : { durationMs: options.durationMs },
|
|
589
|
+
...options.method === void 0 ? {} : { "http.request.method": options.method },
|
|
590
|
+
...options.url === void 0 ? {} : { "url.full": options.url },
|
|
591
|
+
...options.route === void 0 ? {} : { "http.route": options.route },
|
|
592
|
+
...options.route === void 0 ? {} : { "url.path": options.route },
|
|
593
|
+
...options.traceparent === void 0 ? {} : { traceparent: options.traceparent }
|
|
594
|
+
};
|
|
595
|
+
if (options.error !== void 0) fields.error = serializeLogError(options.error);
|
|
596
|
+
return fields;
|
|
597
|
+
}
|
|
598
|
+
/** Log an HTTP response at info/warn/error severity based on status code. */
|
|
599
|
+
function logHttpResponse(message, options) {
|
|
600
|
+
const level = resolveEffectLogLevelFromHttpStatus(options.status);
|
|
601
|
+
const fields = buildHttpResponseLogFields(options);
|
|
602
|
+
const correlationFields = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== void 0).map(([key, value]) => [key, stringifyLogFieldValue(value)]));
|
|
603
|
+
return annotateLogsWithActiveSpanCorrelation(logAtLevel(level, message, fields), correlationFields);
|
|
604
|
+
}
|
|
605
|
+
function logHttpRequestFailure(message, error, fields) {
|
|
606
|
+
const logFields = {};
|
|
607
|
+
if (fields) {
|
|
608
|
+
for (const [key, value] of Object.entries(fields)) if (value !== void 0) logFields[key] = String(value);
|
|
609
|
+
}
|
|
610
|
+
return annotateLogsWithActiveSpanCorrelation(logError(message, {
|
|
611
|
+
error: serializeLogError(error),
|
|
612
|
+
...fields
|
|
613
|
+
}), logFields);
|
|
614
|
+
}
|
|
615
|
+
function normalizeHeaderRecord(headers) {
|
|
616
|
+
const normalized = {};
|
|
617
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
618
|
+
if (value === void 0) continue;
|
|
619
|
+
normalized[key.toLowerCase()] = typeof value === "string" ? value : value[0] ?? "";
|
|
620
|
+
}
|
|
621
|
+
return normalized;
|
|
622
|
+
}
|
|
623
|
+
function normalizeCarrierRecord(carrier) {
|
|
624
|
+
if (!carrier) return void 0;
|
|
625
|
+
const normalized = {};
|
|
626
|
+
for (const [key, value] of Object.entries(carrier)) {
|
|
627
|
+
if (value === void 0 || value === "") continue;
|
|
628
|
+
normalized[key.toLowerCase()] = value;
|
|
629
|
+
}
|
|
630
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
631
|
+
}
|
|
632
|
+
/** Resolve parent span context from a W3C trace carrier. */
|
|
633
|
+
function spanContextFromCarrier(carrier) {
|
|
634
|
+
const normalized = normalizeCarrierRecord(carrier);
|
|
635
|
+
if (!normalized) return void 0;
|
|
636
|
+
const extracted = propagation.extract(context.active(), normalized);
|
|
637
|
+
const spanContext = trace.getSpan(extracted)?.spanContext();
|
|
638
|
+
if (spanContext?.traceId && spanContext.traceId !== "00000000000000000000000000000000" && spanContext.spanId && spanContext.spanId !== "0000000000000000") return spanContext;
|
|
639
|
+
return spanContextFromTraceparent(normalized.traceparent);
|
|
640
|
+
}
|
|
641
|
+
function withParentFromTraceCarrier(carrier, program) {
|
|
642
|
+
const parentSpanContext = spanContextFromCarrier(carrier);
|
|
643
|
+
if (parentSpanContext === void 0) return program;
|
|
644
|
+
return OtelTracer.withSpanContext(program, parentSpanContext);
|
|
645
|
+
}
|
|
646
|
+
/** Build W3C trace carrier for queue/job propagation from request headers + active OTel context. */
|
|
647
|
+
function buildQueueTraceCarrier(headers) {
|
|
648
|
+
const normalized = headers ? normalizeHeaderRecord(headers) : {};
|
|
649
|
+
const parentContext = propagation.extract(context.active(), normalized);
|
|
650
|
+
const carrier = {};
|
|
651
|
+
propagation.inject(parentContext, carrier);
|
|
652
|
+
return Object.keys(carrier).length > 0 ? carrier : void 0;
|
|
653
|
+
}
|
|
654
|
+
/** Build W3C trace carrier using the active Effect span when available. */
|
|
655
|
+
function buildQueueTraceCarrierEffect(headers) {
|
|
656
|
+
return Effect.gen(function* () {
|
|
657
|
+
const normalized = headers ? normalizeHeaderRecord(headers) : {};
|
|
658
|
+
const currentSpan = yield* Effect.currentSpan.pipe(Effect.option);
|
|
659
|
+
if (Option$1.isSome(currentSpan)) return buildTraceCarrierFromSpan(currentSpan.value, normalized);
|
|
660
|
+
return buildQueueTraceCarrier(headers);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/server/jobs.ts
|
|
665
|
+
function resolveParentSpanContext(options) {
|
|
666
|
+
if (options.parentSpanContext !== void 0) return options.parentSpanContext;
|
|
667
|
+
return spanContextFromCarrier(options.parentTraceCarrier);
|
|
668
|
+
}
|
|
669
|
+
function jobWithSpanEffect(options, program) {
|
|
670
|
+
const parentSpanContext = resolveParentSpanContext(options);
|
|
671
|
+
const startedAt = Date.now();
|
|
672
|
+
let wrapped = program.pipe(Effect.tap(() => logInfo("Job completed", {
|
|
673
|
+
durationMs: Date.now() - startedAt,
|
|
674
|
+
"job.name": options.name,
|
|
675
|
+
outcome: "success",
|
|
676
|
+
...options.logAttributes
|
|
677
|
+
})), Effect.tapError((error) => logError("Job failed", {
|
|
678
|
+
durationMs: Date.now() - startedAt,
|
|
679
|
+
error: serializeLogError(error),
|
|
680
|
+
"job.name": options.name,
|
|
681
|
+
outcome: "error",
|
|
682
|
+
...options.logAttributes
|
|
683
|
+
})), (effect) => annotateLogsWithActiveSpanCorrelation(effect, {
|
|
684
|
+
"job.name": options.name,
|
|
685
|
+
...options.logAttributes
|
|
686
|
+
}), Effect.withLogSpan(options.name), Effect.withSpan(options.name, {
|
|
687
|
+
attributes: options.attributes,
|
|
688
|
+
kind: "consumer"
|
|
689
|
+
}));
|
|
690
|
+
if (parentSpanContext !== void 0) wrapped = OtelTracer.withSpanContext(wrapped, parentSpanContext);
|
|
691
|
+
return wrapped;
|
|
692
|
+
}
|
|
693
|
+
function runJobWithSpan(runtime, options, program) {
|
|
694
|
+
return runtime.runPromise(jobWithSpanEffect(options, program));
|
|
695
|
+
}
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region src/server/evlog-bridge.ts
|
|
698
|
+
function headerValue(headers, name) {
|
|
699
|
+
if (!headers) return void 0;
|
|
700
|
+
const direct = headers[name];
|
|
701
|
+
if (direct !== void 0) return direct;
|
|
702
|
+
const lower = headers[name.toLowerCase()];
|
|
703
|
+
if (lower !== void 0) return lower;
|
|
704
|
+
}
|
|
705
|
+
function assignTraceFields(event, traceId, spanId, overwrite) {
|
|
706
|
+
if (overwrite || event.trace_id === void 0) event.trace_id = traceId;
|
|
707
|
+
if (overwrite || event.span_id === void 0) event.span_id = spanId;
|
|
708
|
+
if (overwrite || event.traceId === void 0) event.traceId = traceId;
|
|
709
|
+
if (overwrite || event.spanId === void 0) event.spanId = spanId;
|
|
710
|
+
const trace = typeof event.trace === "object" && event.trace !== null ? event.trace : {};
|
|
711
|
+
if (overwrite || trace.trace_id === void 0) trace.trace_id = traceId;
|
|
712
|
+
if (overwrite || trace.span_id === void 0) trace.span_id = spanId;
|
|
713
|
+
if (Object.keys(trace).length > 0) event.trace = trace;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Enrich evlog wide events with trace/span IDs from OTel global context or W3C headers.
|
|
717
|
+
*
|
|
718
|
+
* Use in the MXL evlog enricher chain alongside `createOtelTraceEnricher`.
|
|
719
|
+
*/
|
|
720
|
+
function createEffectTraceEnricher(options = {}) {
|
|
721
|
+
const overwrite = options.overwrite ?? false;
|
|
722
|
+
return (ctx) => {
|
|
723
|
+
const traceparent = headerValue(ctx.headers, "traceparent");
|
|
724
|
+
const correlation = getEffectTraceCorrelation({ traceparent });
|
|
725
|
+
if (correlation.traceId && correlation.spanId) {
|
|
726
|
+
assignTraceFields(ctx.event, correlation.traceId, correlation.spanId, overwrite);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const parsed = parseTraceparent(traceparent);
|
|
730
|
+
if (parsed.traceId && parsed.spanId) assignTraceFields(ctx.event, parsed.traceId, parsed.spanId, overwrite);
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/server/dependency-spans.ts
|
|
735
|
+
const MAX_FLUSH_SPAN_LINKS = 32;
|
|
736
|
+
function dbClientSpanAttributes(dbSystem, operation) {
|
|
737
|
+
return {
|
|
738
|
+
"db.system": dbSystem,
|
|
739
|
+
"db.system.name": dbSystem,
|
|
740
|
+
"peer.service": dbSystem,
|
|
741
|
+
...operation ? { "db.operation.name": operation } : {}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function queueProducerSpanAttributes(destinationName, peerService = "mxl-worker") {
|
|
745
|
+
return {
|
|
746
|
+
"messaging.destination.name": destinationName,
|
|
747
|
+
"messaging.system": "bullmq",
|
|
748
|
+
"peer.service": peerService
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function queueConsumerSpanAttributes(destinationName) {
|
|
752
|
+
return {
|
|
753
|
+
"messaging.destination.name": destinationName,
|
|
754
|
+
"messaging.system": "bullmq"
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function kafkaProducerSpanAttributes(topic, peerService = "kafka") {
|
|
758
|
+
return {
|
|
759
|
+
"messaging.destination.name": topic,
|
|
760
|
+
"messaging.system": "kafka",
|
|
761
|
+
"peer.service": peerService
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function bufferSpanAttributes(bufferName, operation) {
|
|
765
|
+
return {
|
|
766
|
+
"buffer.name": bufferName,
|
|
767
|
+
"db.operation.name": operation,
|
|
768
|
+
"db.system": "redis",
|
|
769
|
+
"db.system.name": "redis",
|
|
770
|
+
"peer.service": "redis"
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function geoSpanAttributes() {
|
|
774
|
+
return {
|
|
775
|
+
"db.operation.name": "resolve",
|
|
776
|
+
"db.system": "maxmind",
|
|
777
|
+
"db.system.name": "maxmind",
|
|
778
|
+
"peer.service": "maxmind"
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function addSpanLinksFromTraceCarriers(carriers) {
|
|
782
|
+
const span = trace.getActiveSpan();
|
|
783
|
+
if (!span) return 0;
|
|
784
|
+
const seen = /* @__PURE__ */ new Set();
|
|
785
|
+
let linked = 0;
|
|
786
|
+
for (const carrier of carriers) {
|
|
787
|
+
if (!carrier.traceparent || seen.has(carrier.traceparent)) continue;
|
|
788
|
+
seen.add(carrier.traceparent);
|
|
789
|
+
const extracted = propagation.extract(context.active(), carrier);
|
|
790
|
+
const linkedContext = trace.getSpanContext(extracted);
|
|
791
|
+
if (!linkedContext) continue;
|
|
792
|
+
span.addLink({ context: linkedContext });
|
|
793
|
+
linked += 1;
|
|
794
|
+
if (linked >= MAX_FLUSH_SPAN_LINKS) break;
|
|
795
|
+
}
|
|
796
|
+
return linked;
|
|
797
|
+
}
|
|
798
|
+
function withDependencySpan(name, attributesOrOptions, program) {
|
|
799
|
+
const options = isWithDependencySpanOptions(attributesOrOptions) ? attributesOrOptions : { attributes: attributesOrOptions };
|
|
800
|
+
const attributes = options.attributes ?? {};
|
|
801
|
+
const kind = options.kind ?? "client";
|
|
802
|
+
return program.pipe(Effect.withSpan(name, {
|
|
803
|
+
attributes,
|
|
804
|
+
kind
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
function isWithDependencySpanOptions(value) {
|
|
808
|
+
return typeof value === "object" && ("kind" in value || "attributes" in value && typeof value.attributes === "object");
|
|
809
|
+
}
|
|
810
|
+
/** Promise-based dependency span for non-Effect code paths (CH buffers, Kafka produce). */
|
|
811
|
+
async function withPromiseDependencySpan(name, attributes, operation, kind = "client") {
|
|
812
|
+
const tracer = trace.getTracer("mxl-effect-sdk");
|
|
813
|
+
const spanKind = kind === "producer" ? SpanKind.PRODUCER : kind === "consumer" ? SpanKind.CONSUMER : SpanKind.CLIENT;
|
|
814
|
+
return tracer.startActiveSpan(name, {
|
|
815
|
+
attributes,
|
|
816
|
+
kind: spanKind
|
|
817
|
+
}, async (span) => {
|
|
818
|
+
try {
|
|
819
|
+
const result = await operation();
|
|
820
|
+
span.end();
|
|
821
|
+
return result;
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (error instanceof Error) {
|
|
824
|
+
span.recordException(error);
|
|
825
|
+
span.setStatus({
|
|
826
|
+
code: SpanStatusCode.ERROR,
|
|
827
|
+
message: error.message
|
|
828
|
+
});
|
|
829
|
+
} else span.setStatus({
|
|
830
|
+
code: SpanStatusCode.ERROR,
|
|
831
|
+
message: String(error)
|
|
832
|
+
});
|
|
833
|
+
span.end();
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
//#endregion
|
|
839
|
+
export { minimumLogLevelLayer as A, layer_exports as B, withParentFromTraceparent as C, logError as D, logDebug as E, getEffectTraceCorrelation as F, normalizeOtlpBaseUrl as G, getAutoPlatformAttributes as H, parseTraceparent as I, otelGlobalContextBridgeLayer as J, buildTraceCarrierFromSpan as K, traceCorrelationLogAnnotations as L, resolveLogLevelFromEnv as M, serializeLogError as N, logInfo as O, annotateLogsWithActiveSpanCorrelation as P, shouldDisableHttpTrace as R, withParentFromTraceCarrier as S, logAtLevel as T, parseOtelResourceAttributes as U, derivePlatformAttributes as V, parseOtlpHeaders as W, wrapTracerWithOtelContextBridge as Y, logHttpResponse as _, kafkaProducerSpanAttributes as a, spanContextFromTraceparent as b, withDependencySpan as c, jobWithSpanEffect as d, runJobWithSpan as f, logHttpRequestFailure as g, finalizeHttpSpan as h, geoSpanAttributes as i, resolveEffectLogLevelFromHttpStatus as j, logWarning as k, withPromiseDependencySpan as l, buildQueueTraceCarrierEffect as m, bufferSpanAttributes as n, queueConsumerSpanAttributes as o, buildQueueTraceCarrier as p, formatTraceparent as q, dbClientSpanAttributes as r, queueProducerSpanAttributes as s, addSpanLinksFromTraceCarriers as t, createEffectTraceEnricher as u, resolveTraceparentFromHeaders as v, buildHttpStatusLogFields as w, withHttpServerSpan as x, spanContextFromCarrier as y, tracerDisabledWhenLayer as z };
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@safaricom-mxl/effect-sdk",
|
|
3
|
+
"version": "0.0.11",
|
|
4
|
+
"description": "MXL OpenTelemetry SDK for Effect applications",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"node": {
|
|
17
|
+
"types": "./src/index.server.ts",
|
|
18
|
+
"import": "./dist/index.server.mjs"
|
|
19
|
+
},
|
|
20
|
+
"default": {
|
|
21
|
+
"types": "./src/index.server.ts",
|
|
22
|
+
"import": "./dist/index.server.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"./server": {
|
|
26
|
+
"types": "./src/server/index.ts",
|
|
27
|
+
"import": "./dist/server/index.mjs",
|
|
28
|
+
"require": "./dist/server/index.mjs",
|
|
29
|
+
"default": "./dist/server/index.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown",
|
|
34
|
+
"check": "tsc --noEmit",
|
|
35
|
+
"clean": "rimraf dist",
|
|
36
|
+
"test": "vitest run"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@effect/opentelemetry": "^4.0.0-beta.105",
|
|
40
|
+
"@opentelemetry/api": "^1.9.1",
|
|
41
|
+
"effect": "^4.0.0-beta.105",
|
|
42
|
+
"std-env": "^4.2.0",
|
|
43
|
+
"uuid": "^11.1.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
47
|
+
"@opentelemetry/sdk-trace-web": "^2.10.0",
|
|
48
|
+
"@types/node": "^26.1.2",
|
|
49
|
+
"tsdown": "^0.22.14",
|
|
50
|
+
"typescript": "^7.0.2",
|
|
51
|
+
"vitest": "^3.2.7"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@effect/opentelemetry": "^4.0.0-beta.104",
|
|
55
|
+
"effect": ">=4.0.0-beta.104"
|
|
56
|
+
},
|
|
57
|
+
"author": "MXL <ServiceAvailability@Safaricom.co.ke>",
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://gitlab.safaricom.co.ke/mxl/apm/mxl-node-sdk.git",
|
|
61
|
+
"directory": "packages/effect-sdk"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
65
|
+
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"mxl",
|
|
68
|
+
"effect",
|
|
69
|
+
"opentelemetry",
|
|
70
|
+
"observability",
|
|
71
|
+
"telemetry"
|
|
72
|
+
]
|
|
73
|
+
}
|