@safaricom-mxl/effect-sdk 0.0.23 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,109 +1,45 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { Cause, Config, Context, Duration, Effect, Layer, ManagedRuntime, Match, Option, References, Schedule, Schema, Semaphore } from "effect";
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.mjs";
2
+ import { Cause, Config, Context, Duration, Effect, Layer, ManagedRuntime, Match, Option, Predicate, References, Schedule, Schema, Semaphore } from "effect";
3
+ import * as Otel from "@opentelemetry/api";
4
+ import { SpanKind, SpanStatusCode, TraceFlags, context, propagation, trace } from "@opentelemetry/api";
3
5
  import { FetchHttpClient, HttpClient, HttpClientRequest, HttpMiddleware } from "effect/http";
6
+ import { SchemaJITCompiler } from "effect/schema";
7
+ import * as Option$1 from "effect/Option";
4
8
  import { Otlp } from "effect/observability";
5
9
  import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
6
- import * as Otel from "@opentelemetry/api";
7
- import { SpanKind, SpanStatusCode, TraceFlags, context, propagation, trace } from "@opentelemetry/api";
8
- import * as Tracer from "effect/Tracer";
10
+ import * as Tracer$1 from "effect/Tracer";
9
11
  import { v7 } from "uuid";
10
12
  import { platform, provider, runtime } from "std-env";
11
- import { SchemaJITCompiler } from "effect/schema";
12
- import * as Option$1 from "effect/Option";
13
13
  import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
14
- //#region src/server/otel-context-bridge.ts
15
- const evaluate = "~effect/Effect/evaluate";
16
- const contextManagerProbe = Otel.createContextKey("mxl.effect-sdk.context-manager-probe");
17
- let contextManagerInstalled = false;
18
- function hasWorkingContextManager() {
19
- return Otel.context.with(Otel.ROOT_CONTEXT.setValue(contextManagerProbe, true), () => Otel.context.active().getValue(contextManagerProbe) === true);
20
- }
21
- /**
22
- * Install AsyncLocalStorage so `Otel.context.with` actually propagates.
23
- * Does not replace a context manager the host app already registered.
24
- */
25
- function ensureOtelContextManager() {
26
- if (contextManagerInstalled) return;
27
- if (hasWorkingContextManager()) {
28
- contextManagerInstalled = true;
29
- return;
30
- }
31
- const manager = new AsyncLocalStorageContextManager();
32
- manager.enable();
33
- if (!Otel.context.setGlobalContextManager(manager)) {
34
- manager.disable();
35
- contextManagerInstalled = true;
36
- return;
37
- }
38
- contextManagerInstalled = true;
39
- }
40
- function formatTraceparent(span) {
41
- const flags = span.sampled ? "01" : "00";
42
- return `00-${span.traceId}-${span.spanId}-${flags}`;
43
- }
44
- function buildTraceCarrierFromSpan(span, headers) {
45
- const carrier = { traceparent: formatTraceparent(span) };
46
- if (headers?.["tracestate"]) carrier["tracestate"] = headers["tracestate"];
47
- if (headers?.["baggage"]) carrier["baggage"] = headers["baggage"];
48
- return carrier;
49
- }
50
- /**
51
- * Put Effect span ids on the global OpenTelemetry context.
52
- *
53
- * Id-only `setSpanContext`. Live `Otel.Span` objects come from
54
- * `OtelTracer.currentOtelSpan` in Effect space, not from a private TypeId.
55
- */
56
- function effectSpanToOtelContext(activeContext, span) {
57
- return Otel.trace.setSpanContext(activeContext, {
58
- isRemote: span._tag === "ExternalSpan",
59
- spanId: span.spanId,
60
- traceFlags: span.sampled ? Otel.TraceFlags.SAMPLED : Otel.TraceFlags.NONE,
61
- traceId: span.traceId
62
- });
63
- }
64
- /**
65
- * Wrap a tracer so Effect spans activate global `@opentelemetry/api` context.
66
- *
67
- * Prefer this over `Otlp.layerJson({ tracerContext })` until Effect changes
68
- * that hook. On effect@4.0.2 the callback is still `(primitive, span)`
69
- * and must return the evaluation, but `EffectPrimitive` only evaluates with a
70
- * `fiber`. Keep this wrap until Effect passes fiber or evaluates after the hook.
71
- */
72
- function wrapTracerWithOtelContextBridge(base) {
73
- ensureOtelContextManager();
74
- const existingContext = base.context;
75
- return Tracer.make({
76
- span(options) {
77
- return base.span(options);
78
- },
79
- context(primitive, fiber) {
80
- if (fiber.cache.span === void 0) {
81
- if (existingContext) return existingContext(primitive, fiber);
82
- return primitive[evaluate](fiber);
83
- }
84
- return Otel.context.with(effectSpanToOtelContext(Otel.context.active(), fiber.cache.span), () => {
85
- if (existingContext) return existingContext(primitive, fiber);
86
- return primitive[evaluate](fiber);
87
- });
88
- }
89
- });
90
- }
91
- /**
92
- * Layer that wraps the configured Effect tracer with a global OTel context bridge.
93
- *
94
- * Must be provided over `Otlp.layerJson` so queue propagation, log correlation,
95
- * and outbound W3C headers see the active server span.
96
- */
97
- const otelGlobalContextBridgeLayer = Layer.effect(Tracer.Tracer, Effect.gen(function* () {
98
- return wrapTracerWithOtelContextBridge(yield* Tracer.Tracer);
99
- }));
100
- //#endregion
101
- //#region src/server/otlp.ts
102
- function normalizeOtlpBaseUrl(url) {
103
- const trimmed = url.replace(/\/$/, "");
104
- if (trimmed.endsWith("/v1/traces") || trimmed.endsWith("/v1/logs")) return trimmed.replace(/\/v1\/(traces|logs|metrics)$/, "");
105
- return trimmed;
106
- }
14
+ // src/shared/span-options.ts
15
+ const prefixMatcher = (prefixes) => {
16
+ const list = prefixes === void 0 ? [] : [...prefixes];
17
+ return list.length > 0 ? (name) => list.some((prefix) => name.startsWith(prefix)) : void 0;
18
+ };
19
+ const failureIdentifier = (error) => {
20
+ if (Predicate.hasProperty(error, "_tag") && typeof error._tag === "string") return error._tag;
21
+ if (Predicate.hasProperty(error, "name") && typeof error.name === "string") return error.name;
22
+ const body = Predicate.hasProperty(error, "error") ? error.error : void 0;
23
+ if (Predicate.hasProperty(body, "_tag") && typeof body._tag === "string") return body._tag;
24
+ };
25
+ const resolveSpanFilter = (input = {}) => {
26
+ const dropName = prefixMatcher(input.dropSpanNames);
27
+ const dropPredicate = input.dropSpan;
28
+ const drop = dropName !== void 0 && dropPredicate !== void 0 ? (span) => dropName(span.name) || dropPredicate(span) : dropName !== void 0 ? (span) => dropName(span.name) : dropPredicate;
29
+ const identifiers = /* @__PURE__ */ new Set([...input.anticipatedErrorIdentifiers ?? [], ...input.anticipatedErrorTags ?? []]);
30
+ const byIdentifier = identifiers.size > 0 ? (error) => {
31
+ const identifier = failureIdentifier(error);
32
+ return identifier !== void 0 && identifiers.has(identifier);
33
+ } : void 0;
34
+ const byPredicate = input.isAnticipatedError;
35
+ const isAnticipated = byIdentifier !== void 0 && byPredicate !== void 0 ? (error) => byIdentifier(error) || byPredicate(error) : byIdentifier ?? byPredicate;
36
+ return {
37
+ dropSubtree: prefixMatcher(input.dropSpanSubtrees),
38
+ drop,
39
+ isAnticipated,
40
+ includeCauseInStack: input.includeCauseInStack ?? false
41
+ };
42
+ };
107
43
  /** Resolve the OTLP exporter endpoint from standard env vars. */
108
44
  const endpoint = Effect.gen(function* () {
109
45
  const otlp = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_ENDPOINT"));
@@ -155,36 +91,547 @@ const otelResourceAttributes = Effect.gen(function* () {
155
91
  onNone: () => ({}),
156
92
  onSome: (value) => parseOtelResourceAttributes(value)
157
93
  });
158
- });
94
+ });
95
+ /**
96
+ * Parse `OTEL_EXPORTER_OTLP_HEADERS` (`key=value,key2=value2`) into a header map.
97
+ */
98
+ const parseOtlpHeaders = (input) => {
99
+ const result = {};
100
+ for (const pair of input.split(",")) {
101
+ const eq = pair.indexOf("=");
102
+ if (eq === -1) continue;
103
+ const key = pair.slice(0, eq).trim();
104
+ if (!key) continue;
105
+ const raw = pair.slice(eq + 1).trim();
106
+ try {
107
+ result[key] = decodeURIComponent(raw);
108
+ } catch {
109
+ result[key] = raw;
110
+ }
111
+ }
112
+ return result;
113
+ };
114
+ /** Resolve OTLP export headers from env. */
115
+ const otlpHeaders = Effect.gen(function* () {
116
+ const raw = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_HEADERS"));
117
+ return Option.match(raw, {
118
+ onNone: () => ({}),
119
+ onSome: (value) => parseOtlpHeaders(value)
120
+ });
121
+ });
122
+ // src/shared/try-sync.ts
123
+ /** Run a throwing synchronous call, `None` if it threw. */
124
+ const trySync = (thunk) => Effect.runSync(Effect.option(Effect.try(thunk)));
125
+ /** Run a throwing synchronous call, `undefined` if it threw. */
126
+ const trySyncOrUndefined = (thunk) => Option.getOrUndefined(trySync(thunk));
127
+ // src/server/container.ts
128
+ const CONTAINER_ID_RE = /\/docker\/containers\/([0-9a-f]{64})\//;
129
+ const CGROUP_ID_RE = /([0-9a-f]{64})/;
130
+ const SHORT_ID_HOSTNAME_RE = /^[0-9a-f]{12}$/;
131
+ /**
132
+ * Pure derivation from probed inputs — the testable core. Every probe call is
133
+ * guarded by the caller (`getContainerAttributes` wraps the whole thing).
134
+ */
135
+ const deriveContainerAttributes = (probe) => {
136
+ const inDocker = trySyncOrUndefined(() => probe.exists("/.dockerenv")) ?? false;
137
+ const attrs = {};
138
+ if (inDocker) attrs["container.runtime.name"] = "docker";
139
+ const containerId = trySyncOrUndefined(() => probe.readFile("/proc/self/mountinfo").match(CONTAINER_ID_RE)?.[1]) ?? trySyncOrUndefined(() => probe.readFile("/proc/self/cgroup").match(CGROUP_ID_RE)?.[1]) ?? (inDocker ? trySyncOrUndefined(() => {
140
+ const name = probe.hostname();
141
+ return SHORT_ID_HOSTNAME_RE.test(name) ? name : void 0;
142
+ }) : void 0);
143
+ if (containerId) attrs["container.id"] = containerId;
144
+ return attrs;
145
+ };
146
+ let cached;
147
+ /**
148
+ * Read live container identity from the filesystem, memoized for the process
149
+ * lifetime (container identity cannot change under a running process). Returns
150
+ * `{}` anywhere the probes aren't available — non-Linux, workerd, browsers.
151
+ */
152
+ const getContainerAttributes = () => {
153
+ if (cached) return cached;
154
+ const proc = globalThis.process;
155
+ const loadBuiltin = proc?.getBuiltinModule;
156
+ if (proc?.platform !== "linux" || typeof loadBuiltin !== "function") {
157
+ cached = {};
158
+ return cached;
159
+ }
160
+ const fs = trySyncOrUndefined(() => loadBuiltin("node:fs"));
161
+ const os = trySyncOrUndefined(() => loadBuiltin("node:os"));
162
+ if (!fs || !os) {
163
+ cached = {};
164
+ return cached;
165
+ }
166
+ cached = deriveContainerAttributes({
167
+ exists: (path) => fs.existsSync(path),
168
+ readFile: (path) => fs.readFileSync(path, "utf8"),
169
+ hostname: () => os.hostname()
170
+ });
171
+ return cached;
172
+ };
173
+ // src/shared/dependency-spans.ts
174
+ const MAX_FLUSH_SPAN_LINKS = 32;
175
+ function dbClientSpanAttributes(dbSystem, operation) {
176
+ return {
177
+ "db.system": dbSystem,
178
+ "db.system.name": dbSystem,
179
+ "peer.service": dbSystem,
180
+ ...operation ? { "db.operation.name": operation } : {}
181
+ };
182
+ }
183
+ function queueProducerSpanAttributes(destinationName, peerService = "mxl-worker") {
184
+ return {
185
+ "messaging.destination.name": destinationName,
186
+ "messaging.system": "bullmq",
187
+ "peer.service": peerService
188
+ };
189
+ }
190
+ function queueConsumerSpanAttributes(destinationName) {
191
+ return {
192
+ "messaging.destination.name": destinationName,
193
+ "messaging.system": "bullmq"
194
+ };
195
+ }
196
+ function kafkaProducerSpanAttributes(topic, peerService = "kafka") {
197
+ return {
198
+ "messaging.destination.name": topic,
199
+ "messaging.system": "kafka",
200
+ "peer.service": peerService
201
+ };
202
+ }
203
+ function bufferSpanAttributes(bufferName, operation) {
204
+ return {
205
+ "buffer.name": bufferName,
206
+ "db.operation.name": operation,
207
+ "db.system": "redis",
208
+ "db.system.name": "redis",
209
+ "peer.service": "redis"
210
+ };
211
+ }
212
+ function geoSpanAttributes() {
213
+ return {
214
+ "db.operation.name": "resolve",
215
+ "db.system": "maxmind",
216
+ "db.system.name": "maxmind",
217
+ "peer.service": "maxmind"
218
+ };
219
+ }
220
+ function addSpanLinksFromTraceCarriers(carriers) {
221
+ const span = trace.getActiveSpan();
222
+ if (!span) return 0;
223
+ const seen = /* @__PURE__ */ new Set();
224
+ let linked = 0;
225
+ for (const carrier of carriers) {
226
+ if (!carrier.traceparent || seen.has(carrier.traceparent)) continue;
227
+ seen.add(carrier.traceparent);
228
+ const extracted = propagation.extract(context.active(), carrier);
229
+ const linkedContext = trace.getSpanContext(extracted);
230
+ if (!linkedContext) continue;
231
+ span.addLink({ context: linkedContext });
232
+ linked += 1;
233
+ if (linked >= MAX_FLUSH_SPAN_LINKS) break;
234
+ }
235
+ return linked;
236
+ }
237
+ function withDependencySpan(name, attributesOrOptions, program) {
238
+ const options = isWithDependencySpanOptions(attributesOrOptions) ? attributesOrOptions : { attributes: attributesOrOptions };
239
+ const attributes = options.attributes ?? {};
240
+ const kind = options.kind ?? "client";
241
+ return program.pipe(Effect.withSpan(name, {
242
+ attributes,
243
+ kind
244
+ }));
245
+ }
246
+ function isWithDependencySpanOptions(value) {
247
+ return typeof value === "object" && ("kind" in value || "attributes" in value && typeof value.attributes === "object");
248
+ }
249
+ /** Promise-based dependency span for non-Effect code paths. */
250
+ async function withPromiseDependencySpan(name, attributes, operation, kind = "client") {
251
+ const tracer = trace.getTracer("mxl-effect-sdk");
252
+ const spanKind = kind === "producer" ? SpanKind.PRODUCER : kind === "consumer" ? SpanKind.CONSUMER : SpanKind.CLIENT;
253
+ return tracer.startActiveSpan(name, {
254
+ attributes,
255
+ kind: spanKind
256
+ }, async (span) => {
257
+ try {
258
+ const result = await operation();
259
+ span.end();
260
+ return result;
261
+ } catch (error) {
262
+ if (error instanceof Error) {
263
+ span.recordException(error);
264
+ span.setStatus({
265
+ code: SpanStatusCode.ERROR,
266
+ message: error.message
267
+ });
268
+ } else span.setStatus({
269
+ code: SpanStatusCode.ERROR,
270
+ message: String(error)
271
+ });
272
+ span.end();
273
+ throw error;
274
+ }
275
+ });
276
+ }
277
+ const RESERVED_EVENT_NAMES = ["session_start", "session_end"];
278
+ const isReservedEventName = (name) => RESERVED_EVENT_NAMES.some((reserved) => reserved === name);
279
+ const coerceTrackProps = (props) => {
280
+ if (props === void 0) return {};
281
+ const attributes = {};
282
+ for (const [key, value] of Object.entries(props)) {
283
+ if (value === void 0) continue;
284
+ attributes[key] = typeof value === "string" ? value : String(value);
285
+ }
286
+ return attributes;
287
+ };
288
+ const toEventTimestamp = (value) => (value instanceof Date ? value : new Date(value ?? Date.now())).toISOString();
289
+ const encodeProductEventLine = (input) => {
290
+ const trimmed = typeof input.name === "string" ? input.name.trim() : "";
291
+ if (trimmed.length === 0 || isReservedEventName(trimmed)) return;
292
+ const options = input.options ?? {};
293
+ return {
294
+ attributes: coerceTrackProps(options.attributes),
295
+ group_id: options.groupId ?? "",
296
+ name: trimmed.slice(0, 80),
297
+ page_path: options.pagePath ?? "",
298
+ service_name: input.serviceName,
299
+ session_id: options.sessionId ?? "",
300
+ source: input.source,
301
+ timestamp: toEventTimestamp(options.timestamp),
302
+ traceparent: options.traceparent,
303
+ url: options.url ?? "",
304
+ user_id: options.userId ?? "",
305
+ visitor_id: options.visitorId ?? ""
306
+ };
307
+ };
308
+ const encodeNdjson = (lines) => `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`;
309
+ // src/version.ts
310
+ const SDK_NAME = "mxl-effect-sdk";
311
+ const SDK_VERSION = "0.0.24";
312
+ // src/server/events.ts
313
+ var events_exports = /* @__PURE__ */ __exportAll({
314
+ MxlEvents: () => MxlEvents,
315
+ ProductEventsPostError: () => ProductEventsPostError,
316
+ layer: () => layer$1,
317
+ make: () => make$1,
318
+ makeHandle: () => makeHandle
319
+ });
320
+ const DEFAULT_FLUSH_INTERVAL = Duration.seconds(5);
321
+ const DEFAULT_MAX_BATCH_SIZE = 100;
322
+ const MAX_BUFFERED_EVENTS = 1e3;
323
+ const WARN_INTERVAL_MS = 3e4;
324
+ const DEFAULT_BASKET_URL = "http://127.0.0.1:8000";
325
+ var MxlEvents = class extends Context.Service()("@safaricom-mxl/effect-sdk/MxlEvents") {};
326
+ var ProductEventsPostError = class extends Schema.TaggedError()("@safaricom-mxl/effect-sdk/ProductEventsPostError", {
327
+ cause: Schema.optionalKey(Schema.Defect()),
328
+ message: Schema.String,
329
+ status: Schema.optionalKey(Schema.Number)
330
+ }) {};
331
+ SchemaJITCompiler.enable(ProductEventsPostError.ast);
332
+ const stringOrUndefined$1 = (value) => value && value.length > 0 ? value : void 0;
333
+ const resolveEventsConfig = (config) => {
334
+ const endpoint = (config.endpoint ?? process.env["MXL_BASKET_URL"] ?? DEFAULT_BASKET_URL).replace(/\/$/, "");
335
+ return {
336
+ apiKey: stringOrUndefined$1(config.apiKey ?? process.env["MXL_API_KEY"]),
337
+ clientId: stringOrUndefined$1(config.clientId ?? process.env["MXL_CLIENT_ID"]),
338
+ serviceName: config.serviceName ?? process.env["OTEL_SERVICE_NAME"] ?? "unknown",
339
+ url: `${endpoint}${config.eventsPath ?? "/events"}`
340
+ };
341
+ };
342
+ const make$1 = Effect.fn("MxlEvents.make")(function* (config = {}) {
343
+ const resolved = resolveEventsConfig(config);
344
+ const maxBatchSize = Math.max(1, config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE);
345
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
346
+ const flushing = yield* Semaphore.make(1);
347
+ let buffer = [];
348
+ let warnedAboutName = false;
349
+ let warnedAboutClient = false;
350
+ let lastPostWarnAt = 0;
351
+ const warnPost = (error) => {
352
+ const now = Date.now();
353
+ if (now - lastPostWarnAt < WARN_INTERVAL_MS) return;
354
+ lastPostWarnAt = now;
355
+ console.warn(`[MxlEvents] ${error.message} (dropping batch)`, error.cause ?? "");
356
+ };
357
+ const warnMissingClient = () => {
358
+ if (warnedAboutClient) return;
359
+ warnedAboutClient = true;
360
+ console.warn("[MxlEvents] no client id — set MXL_CLIENT_ID or pass `clientId`; dropping events");
361
+ };
362
+ const warnEmptyName = () => {
363
+ if (warnedAboutName) return;
364
+ warnedAboutName = true;
365
+ console.warn("[MxlEvents] track() needs a non-empty event name; the call was ignored.");
366
+ };
367
+ const post = (lines) => {
368
+ const headers = {
369
+ "mxl-client-id": resolved.clientId ?? "",
370
+ "mxl-sdk-name": SDK_NAME,
371
+ "mxl-sdk-version": SDK_VERSION,
372
+ "user-agent": `${SDK_NAME}-server/${SDK_VERSION}`
373
+ };
374
+ if (resolved.apiKey) headers["mxl-api-key"] = resolved.apiKey;
375
+ return HttpClientRequest.post(resolved.url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(encodeNdjson(lines), "application/x-ndjson"), client.execute, Effect.asVoid, Effect.mapError((error) => error.response !== void 0 ? new ProductEventsPostError({
376
+ message: `POST ${resolved.url} → ${error.response.status}`,
377
+ status: error.response.status
378
+ }) : new ProductEventsPostError({
379
+ cause: error,
380
+ message: `POST ${resolved.url} failed: ${error.message}`
381
+ })));
382
+ };
383
+ const flush = flushing.withPermits(1)(Effect.suspend(() => {
384
+ if (buffer.length === 0) return Effect.void;
385
+ const batch = buffer;
386
+ buffer = [];
387
+ if (resolved.clientId === void 0) {
388
+ warnMissingClient();
389
+ return Effect.void;
390
+ }
391
+ return post(batch).pipe(Effect.catchCause((cause) => Effect.sync(() => {
392
+ warnPost(Option.getOrElse(Cause.findErrorOption(cause), () => new ProductEventsPostError({
393
+ cause: Cause.squash(cause),
394
+ message: `POST ${resolved.url} failed`
395
+ })));
396
+ })));
397
+ }));
398
+ const track = (name, options = {}) => Effect.sync(() => {
399
+ const line = encodeProductEventLine({
400
+ name,
401
+ options,
402
+ serviceName: resolved.serviceName,
403
+ source: "server"
404
+ });
405
+ if (line === void 0) {
406
+ warnEmptyName();
407
+ return false;
408
+ }
409
+ buffer.push(line);
410
+ if (buffer.length > MAX_BUFFERED_EVENTS) buffer.splice(0, buffer.length - MAX_BUFFERED_EVENTS);
411
+ return buffer.length >= maxBatchSize;
412
+ }).pipe(Effect.flatMap((full) => full ? Effect.forkDetach(flush) : Effect.void), Effect.asVoid);
413
+ const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
414
+ if (Duration.toMillis(interval) > 0) yield* Effect.forkScoped(flush.pipe(Effect.schedule(Schedule.spaced(interval))));
415
+ yield* Effect.addFinalizer(() => flush);
416
+ return {
417
+ flush,
418
+ track
419
+ };
420
+ });
421
+ const layer$1 = (config = {}) => Layer.effect(MxlEvents, make$1(config)).pipe(Layer.provide(FetchHttpClient.layer));
422
+ const makeHandle = (config = {}) => {
423
+ const runtime = ManagedRuntime.make(layer$1(config));
424
+ const swallow = (promise) => promise.then(() => void 0, () => void 0);
425
+ return {
426
+ dispose: () => swallow(runtime.dispose()),
427
+ flush: () => swallow(runtime.runPromise(Effect.gen(function* () {
428
+ yield* (yield* MxlEvents).flush;
429
+ }))),
430
+ track: (name, options) => {
431
+ swallow(runtime.runPromise(Effect.gen(function* () {
432
+ yield* (yield* MxlEvents).track(name, options);
433
+ })));
434
+ }
435
+ };
436
+ };
437
+ // src/server/trace-correlation.ts
438
+ const ZERO_TRACE_ID = "00000000000000000000000000000000";
439
+ const ZERO_SPAN_ID = "0000000000000000";
440
+ /** Parse W3C `traceparent` (`00-{traceId}-{spanId}-{flags}`). */
441
+ function parseTraceparent(traceparent) {
442
+ if (!traceparent) return {};
443
+ const parts = traceparent.trim().split("-");
444
+ if (parts.length < 4) return {};
445
+ const traceId = parts[1];
446
+ const spanId = parts[2];
447
+ if (!traceId || !spanId) return {};
448
+ if (traceId.length !== 32 || spanId.length !== 16) return {};
449
+ if (traceId === ZERO_TRACE_ID || spanId === ZERO_SPAN_ID) return {};
450
+ return {
451
+ spanId,
452
+ traceId
453
+ };
454
+ }
455
+ /**
456
+ * Resolve trace/span IDs for log correlation.
457
+ *
458
+ * Prefers the active OpenTelemetry span (when a host bridge or queue propagation
459
+ * is active), then falls back to a W3C `traceparent` carrier.
460
+ */
461
+ function getEffectTraceCorrelation(options) {
462
+ const activeSpan = trace.getSpan(context.active());
463
+ if (activeSpan) {
464
+ const spanContext = activeSpan.spanContext();
465
+ if (spanContext.traceId && spanContext.traceId !== ZERO_TRACE_ID && spanContext.spanId && spanContext.spanId !== ZERO_SPAN_ID) return {
466
+ spanId: spanContext.spanId,
467
+ traceId: spanContext.traceId
468
+ };
469
+ }
470
+ return parseTraceparent(options?.traceparent);
471
+ }
472
+ /** Build log annotation fields from active span correlation. */
473
+ function traceCorrelationLogAnnotations(options) {
474
+ const { traceparent, ...extra } = options ?? {};
475
+ const correlation = getEffectTraceCorrelation({ traceparent });
476
+ const annotations = { ...extra };
477
+ if (correlation.traceId) {
478
+ annotations["trace.trace_id"] = correlation.traceId;
479
+ annotations["trace_id"] = correlation.traceId;
480
+ }
481
+ if (correlation.spanId) {
482
+ annotations["trace.span_id"] = correlation.spanId;
483
+ annotations["span_id"] = correlation.spanId;
484
+ }
485
+ return annotations;
486
+ }
487
+ /** Annotate Effect logs with the active span's trace/span IDs. */
488
+ function annotateLogsWithActiveSpanCorrelation(program, fields) {
489
+ return Effect.gen(function* () {
490
+ const { traceparent, ...extra } = fields ?? {};
491
+ const annotations = { ...extra };
492
+ const currentSpan = yield* Effect.currentSpan.pipe(Effect.option);
493
+ if (Option$1.isSome(currentSpan)) {
494
+ annotations["trace.trace_id"] = currentSpan.value.traceId;
495
+ annotations["trace_id"] = currentSpan.value.traceId;
496
+ annotations["trace.span_id"] = currentSpan.value.spanId;
497
+ annotations["span_id"] = currentSpan.value.spanId;
498
+ } else Object.assign(annotations, traceCorrelationLogAnnotations({
499
+ ...traceparent !== void 0 ? { traceparent } : {},
500
+ ...extra
501
+ }));
502
+ if (Object.keys(annotations).length === 0) return yield* program;
503
+ return yield* program.pipe(Effect.annotateLogs(annotations));
504
+ });
505
+ }
506
+ // src/server/evlog-bridge.ts
507
+ function headerValue$1(headers, name) {
508
+ if (!headers) return void 0;
509
+ const direct = headers[name];
510
+ if (direct !== void 0) return direct;
511
+ const lower = headers[name.toLowerCase()];
512
+ if (lower !== void 0) return lower;
513
+ }
514
+ function assignTraceFields(event, traceId, spanId, overwrite) {
515
+ if (overwrite || event["trace_id"] === void 0) event["trace_id"] = traceId;
516
+ if (overwrite || event["span_id"] === void 0) event["span_id"] = spanId;
517
+ if (overwrite || event["traceId"] === void 0) event["traceId"] = traceId;
518
+ if (overwrite || event["spanId"] === void 0) event["spanId"] = spanId;
519
+ const trace = typeof event["trace"] === "object" && event["trace"] !== null ? event["trace"] : {};
520
+ if (overwrite || trace["trace_id"] === void 0) trace["trace_id"] = traceId;
521
+ if (overwrite || trace["span_id"] === void 0) trace["span_id"] = spanId;
522
+ if (Object.keys(trace).length > 0) event["trace"] = trace;
523
+ }
524
+ /**
525
+ * Enrich evlog wide events with trace/span IDs from OTel global context or W3C headers.
526
+ *
527
+ * Use `createEffectTraceEnricher` in the MXL evlog enricher chain.
528
+ */
529
+ function createEffectTraceEnricher(options = {}) {
530
+ const overwrite = options.overwrite ?? false;
531
+ return (ctx) => {
532
+ const traceparent = headerValue$1(ctx.headers, "traceparent");
533
+ const correlation = getEffectTraceCorrelation({ traceparent });
534
+ if (correlation.traceId && correlation.spanId) {
535
+ assignTraceFields(ctx.event, correlation.traceId, correlation.spanId, overwrite);
536
+ return;
537
+ }
538
+ const parsed = parseTraceparent(traceparent);
539
+ if (parsed.traceId && parsed.spanId) assignTraceFields(ctx.event, parsed.traceId, parsed.spanId, overwrite);
540
+ };
541
+ }
542
+ // src/server/otel-context-bridge.ts
543
+ const evaluate = "~effect/Effect/evaluate";
544
+ const contextManagerProbe = Otel.createContextKey("mxl.effect-sdk.context-manager-probe");
545
+ let contextManagerInstalled = false;
546
+ function hasWorkingContextManager() {
547
+ return Otel.context.with(Otel.ROOT_CONTEXT.setValue(contextManagerProbe, true), () => Otel.context.active().getValue(contextManagerProbe) === true);
548
+ }
549
+ /**
550
+ * Install AsyncLocalStorage so `Otel.context.with` actually propagates.
551
+ * Does not replace a context manager the host app already registered.
552
+ */
553
+ function ensureOtelContextManager() {
554
+ if (contextManagerInstalled) return;
555
+ if (hasWorkingContextManager()) {
556
+ contextManagerInstalled = true;
557
+ return;
558
+ }
559
+ const manager = new AsyncLocalStorageContextManager();
560
+ manager.enable();
561
+ if (!Otel.context.setGlobalContextManager(manager)) {
562
+ manager.disable();
563
+ contextManagerInstalled = true;
564
+ return;
565
+ }
566
+ contextManagerInstalled = true;
567
+ }
568
+ function formatTraceparent(span) {
569
+ const flags = span.sampled ? "01" : "00";
570
+ return `00-${span.traceId}-${span.spanId}-${flags}`;
571
+ }
572
+ function buildTraceCarrierFromSpan(span, headers) {
573
+ const carrier = { traceparent: formatTraceparent(span) };
574
+ if (headers?.["tracestate"]) carrier["tracestate"] = headers["tracestate"];
575
+ if (headers?.["baggage"]) carrier["baggage"] = headers["baggage"];
576
+ return carrier;
577
+ }
578
+ /**
579
+ * Put Effect span ids on the global OpenTelemetry context.
580
+ *
581
+ * Id-only `setSpanContext`. Live `Otel.Span` objects come from
582
+ * `OtelTracer.currentOtelSpan` in Effect space, not from a private TypeId.
583
+ */
584
+ function effectSpanToOtelContext(activeContext, span) {
585
+ return Otel.trace.setSpanContext(activeContext, {
586
+ isRemote: span._tag === "ExternalSpan",
587
+ spanId: span.spanId,
588
+ traceFlags: span.sampled ? Otel.TraceFlags.SAMPLED : Otel.TraceFlags.NONE,
589
+ traceId: span.traceId
590
+ });
591
+ }
159
592
  /**
160
- * Parse `OTEL_EXPORTER_OTLP_HEADERS` (`key=value,key2=value2`) into a header map.
593
+ * Wrap a tracer so Effect spans activate global `@opentelemetry/api` context.
594
+ *
595
+ * Prefer this over `Otlp.layerJson({ tracerContext })` until Effect changes
596
+ * that hook. On effect@4.0.6 the callback is still `(primitive, span)`
597
+ * and must return the evaluation, but `EffectPrimitive` only evaluates with a
598
+ * `fiber`. Keep this wrap until Effect passes fiber or evaluates after the hook.
161
599
  */
162
- const parseOtlpHeaders = (input) => {
163
- const result = {};
164
- for (const pair of input.split(",")) {
165
- const eq = pair.indexOf("=");
166
- if (eq === -1) continue;
167
- const key = pair.slice(0, eq).trim();
168
- if (!key) continue;
169
- const raw = pair.slice(eq + 1).trim();
170
- try {
171
- result[key] = decodeURIComponent(raw);
172
- } catch {
173
- result[key] = raw;
600
+ function wrapTracerWithOtelContextBridge(base) {
601
+ ensureOtelContextManager();
602
+ const existingContext = base.context;
603
+ return Tracer$1.make({
604
+ span(options) {
605
+ return base.span(options);
606
+ },
607
+ context(primitive, fiber) {
608
+ if (fiber.cache.span === void 0) {
609
+ if (existingContext) return existingContext(primitive, fiber);
610
+ return primitive[evaluate](fiber);
611
+ }
612
+ return Otel.context.with(effectSpanToOtelContext(Otel.context.active(), fiber.cache.span), () => {
613
+ if (existingContext) return existingContext(primitive, fiber);
614
+ return primitive[evaluate](fiber);
615
+ });
174
616
  }
175
- }
176
- return result;
177
- };
178
- /** Resolve OTLP export headers from env. */
179
- const otlpHeaders = Effect.gen(function* () {
180
- const raw = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_HEADERS"));
181
- return Option.match(raw, {
182
- onNone: () => ({}),
183
- onSome: (value) => parseOtlpHeaders(value)
184
617
  });
185
- });
186
- //#endregion
187
- //#region src/server/platform.ts
618
+ }
619
+ /**
620
+ * Layer that wraps the configured Effect tracer with a global OTel context bridge.
621
+ *
622
+ * Must be provided over `Otlp.layerJson` so queue propagation, log correlation,
623
+ * and outbound W3C headers see the active server span.
624
+ */
625
+ const otelGlobalContextBridgeLayer = Layer.effect(Tracer$1.Tracer, Effect.gen(function* () {
626
+ return wrapTracerWithOtelContextBridge(yield* Tracer$1.Tracer);
627
+ }));
628
+ // src/server/otlp.ts
629
+ function normalizeOtlpBaseUrl(url) {
630
+ const trimmed = url.replace(/\/$/, "");
631
+ if (trimmed.endsWith("/v1/traces") || trimmed.endsWith("/v1/logs")) return trimmed.replace(/\/v1\/(traces|logs|metrics)$/, "");
632
+ return trimmed;
633
+ }
634
+ // src/server/platform.ts
188
635
  const empty = {};
189
636
  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));
190
637
  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));
@@ -296,15 +743,20 @@ const getAutoPlatformAttributes = () => {
296
743
  env
297
744
  });
298
745
  };
299
- //#endregion
300
- //#region src/server/resource.ts
301
- const stringOrUndefined$1 = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
746
+ // src/server/resource.ts
747
+ const stringOrUndefined = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
302
748
  const DEFAULT_VCS_REPOSITORY_URL = "https://gitlab.safaricom.co.ke/mxl/apm/mxl-effect-sdk";
303
749
  function resolveCommitRevision() {
304
- return stringOrUndefined$1(process.env["COMMIT_SHA"]) ?? stringOrUndefined$1(process.env["VERCEL_GIT_COMMIT_SHA"]) ?? stringOrUndefined$1(process.env["GITHUB_SHA"]) ?? stringOrUndefined$1(process.env["RAILWAY_GIT_COMMIT_SHA"]) ?? stringOrUndefined$1(process.env["CF_PAGES_COMMIT_SHA"]) ?? stringOrUndefined$1(process.env["RENDER_GIT_COMMIT"]);
750
+ 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"]);
305
751
  }
306
752
  let serviceInstanceId;
307
753
  const getServiceInstanceId = () => serviceInstanceId ??= v7();
754
+ /**
755
+ * Best-effort guard for `vcs.ref.head.revision`: only stamp values that look
756
+ * like a git commit SHA — `serviceVersion` may legitimately be a semver
757
+ * release string, which belongs in `service.version` but not in `vcs.*`.
758
+ */
759
+ const isCommitSha = (value) => value !== void 0 && /^[0-9a-f]{7,40}$/i.test(value);
308
760
  const resolveResource = (config) => Effect.gen(function* () {
309
761
  if (yield* sdkDisabled) return {
310
762
  endpoint: void 0,
@@ -329,231 +781,68 @@ const resolveResource = (config) => Effect.gen(function* () {
329
781
  const envResourceAttributes = yield* otelResourceAttributes;
330
782
  const attributes = {};
331
783
  Object.assign(attributes, getAutoPlatformAttributes());
784
+ Object.assign(attributes, getContainerAttributes());
332
785
  attributes["mxl.sdk.type"] = config.sdkType ?? "server";
333
786
  attributes["service.instance.id"] = getServiceInstanceId();
334
787
  if (environment$1) {
335
788
  attributes["deployment.environment"] = environment$1;
336
789
  attributes["deployment.environment.name"] = environment$1;
337
- }
338
- if (serviceVersion$1) attributes["service.version"] = serviceVersion$1;
339
- const commitRevision = resolveCommitRevision();
340
- if (commitRevision) {
341
- attributes["deployment.commit_sha"] = commitRevision;
342
- attributes["vcs.ref.head.revision"] = commitRevision;
343
- }
344
- attributes["vcs.repository.url.full"] = stringOrUndefined$1(process.env["MXL_VCS_REPOSITORY_URL"]) ?? DEFAULT_VCS_REPOSITORY_URL;
345
- Object.assign(attributes, envResourceAttributes);
346
- if (config.attributes) Object.assign(attributes, config.attributes);
347
- return {
348
- endpoint: endpoint$1,
349
- environment: environment$1,
350
- headers,
351
- resource: {
352
- serviceName,
353
- serviceVersion: serviceVersion$1,
354
- attributes
355
- }
356
- };
357
- }).pipe(Effect.orDie);
358
- //#endregion
359
- //#region src/server/layer.ts
360
- var layer_exports = /* @__PURE__ */ __exportAll({ layer: () => layer$1 });
361
- /**
362
- * Create an Effect Layer that provides OpenTelemetry traces, logs, and metrics
363
- * configured for MXL's OTLP collector.
364
- *
365
- * Returns a no-op layer when `OTEL_SDK_DISABLED=true`.
366
- *
367
- * Wraps the OTLP tracer with `otelGlobalContextBridgeLayer` via `provideMerge`.
368
- * Measured on effect@4.0.2: `Otlp.layerJson({ tracerContext })` still
369
- * types the hook as `(primitive, span) => X` and Effect returns that result as
370
- * the evaluation, so the callback cannot call `~effect/Effect/evaluate` without
371
- * a `fiber`. `Layer.updateService` also cannot wrap a Tracer the OTLP layer
372
- * already provides. Keep the merge wrap until Effect passes fiber or evaluates
373
- * after a side-effect-only context hook.
374
- */
375
- const layer$1 = (config = {}) => Layer.unwrap(Effect.gen(function* () {
376
- const resolved = yield* resolveResource({
377
- ...config,
378
- sdkType: "server"
379
- });
380
- if (!resolved.endpoint) return Layer.empty;
381
- const otlp = Otlp.layerJson({
382
- baseUrl: normalizeOtlpBaseUrl(resolved.endpoint),
383
- headers: resolved.headers,
384
- resource: resolved.resource,
385
- maxBatchSize: config.maxBatchSize,
386
- loggerExportInterval: config.loggerExportInterval,
387
- metricsExportInterval: config.metricsExportInterval,
388
- tracerExportInterval: config.tracerExportInterval,
389
- shutdownTimeout: config.shutdownTimeout
390
- }).pipe(Layer.provide(FetchHttpClient.layer));
391
- return otelGlobalContextBridgeLayer.pipe(Layer.provideMerge(otlp));
392
- }));
393
- const RESERVED_EVENT_NAMES = ["session_start", "session_end"];
394
- const isReservedEventName = (name) => RESERVED_EVENT_NAMES.some((reserved) => reserved === name);
395
- const coerceTrackProps = (props) => {
396
- if (props === void 0) return {};
397
- const attributes = {};
398
- for (const [key, value] of Object.entries(props)) {
399
- if (value === void 0) continue;
400
- attributes[key] = typeof value === "string" ? value : String(value);
401
- }
402
- return attributes;
403
- };
404
- const toEventTimestamp = (value) => (value instanceof Date ? value : new Date(value ?? Date.now())).toISOString();
405
- const encodeProductEventLine = (input) => {
406
- const trimmed = typeof input.name === "string" ? input.name.trim() : "";
407
- if (trimmed.length === 0 || isReservedEventName(trimmed)) return;
408
- const options = input.options ?? {};
409
- return {
410
- attributes: coerceTrackProps(options.attributes),
411
- group_id: options.groupId ?? "",
412
- name: trimmed.slice(0, 80),
413
- page_path: options.pagePath ?? "",
414
- service_name: input.serviceName,
415
- session_id: options.sessionId ?? "",
416
- source: input.source,
417
- timestamp: toEventTimestamp(options.timestamp),
418
- traceparent: options.traceparent,
419
- url: options.url ?? "",
420
- user_id: options.userId ?? "",
421
- visitor_id: options.visitorId ?? ""
422
- };
423
- };
424
- const encodeNdjson = (lines) => `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`;
425
- //#endregion
426
- //#region src/version.ts
427
- const SDK_NAME = "mxl-effect-sdk";
428
- const SDK_VERSION = "0.0.15";
429
- //#endregion
430
- //#region src/server/events.ts
431
- var events_exports = /* @__PURE__ */ __exportAll({
432
- MxlEvents: () => MxlEvents,
433
- ProductEventsPostError: () => ProductEventsPostError,
434
- layer: () => layer,
435
- make: () => make$1,
436
- makeHandle: () => makeHandle
437
- });
438
- const DEFAULT_FLUSH_INTERVAL = Duration.seconds(5);
439
- const DEFAULT_MAX_BATCH_SIZE = 100;
440
- const MAX_BUFFERED_EVENTS = 1e3;
441
- const WARN_INTERVAL_MS = 3e4;
442
- const DEFAULT_BASKET_URL = "http://127.0.0.1:8000";
443
- var MxlEvents = class extends Context.Service()("@safaricom-mxl/effect-sdk/MxlEvents") {};
444
- var ProductEventsPostError = class extends Schema.TaggedError()("@safaricom-mxl/effect-sdk/ProductEventsPostError", {
445
- cause: Schema.optionalKey(Schema.Defect()),
446
- message: Schema.String,
447
- status: Schema.optionalKey(Schema.Number)
448
- }) {};
449
- SchemaJITCompiler.enable(ProductEventsPostError.ast);
450
- const stringOrUndefined = (value) => value && value.length > 0 ? value : void 0;
451
- const resolveEventsConfig = (config) => {
452
- const endpoint = (config.endpoint ?? process.env["MXL_BASKET_URL"] ?? DEFAULT_BASKET_URL).replace(/\/$/, "");
453
- return {
454
- apiKey: stringOrUndefined(config.apiKey ?? process.env["MXL_API_KEY"]),
455
- clientId: stringOrUndefined(config.clientId ?? process.env["MXL_CLIENT_ID"]),
456
- serviceName: config.serviceName ?? process.env["OTEL_SERVICE_NAME"] ?? "unknown",
457
- url: `${endpoint}${config.eventsPath ?? "/events"}`
458
- };
459
- };
460
- const make$1 = Effect.fn("MxlEvents.make")(function* (config = {}) {
461
- const resolved = resolveEventsConfig(config);
462
- const maxBatchSize = Math.max(1, config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE);
463
- const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
464
- const flushing = yield* Semaphore.make(1);
465
- let buffer = [];
466
- let warnedAboutName = false;
467
- let warnedAboutClient = false;
468
- let lastPostWarnAt = 0;
469
- const warnPost = (error) => {
470
- const now = Date.now();
471
- if (now - lastPostWarnAt < WARN_INTERVAL_MS) return;
472
- lastPostWarnAt = now;
473
- console.warn(`[MxlEvents] ${error.message} (dropping batch)`, error.cause ?? "");
474
- };
475
- const warnMissingClient = () => {
476
- if (warnedAboutClient) return;
477
- warnedAboutClient = true;
478
- console.warn("[MxlEvents] no client id — set MXL_CLIENT_ID or pass `clientId`; dropping events");
479
- };
480
- const warnEmptyName = () => {
481
- if (warnedAboutName) return;
482
- warnedAboutName = true;
483
- console.warn("[MxlEvents] track() needs a non-empty event name; the call was ignored.");
484
- };
485
- const post = (lines) => {
486
- const headers = {
487
- "mxl-client-id": resolved.clientId ?? "",
488
- "mxl-sdk-name": SDK_NAME,
489
- "mxl-sdk-version": SDK_VERSION,
490
- "user-agent": `${SDK_NAME}-server/${SDK_VERSION}`
491
- };
492
- if (resolved.apiKey) headers["mxl-api-key"] = resolved.apiKey;
493
- return HttpClientRequest.post(resolved.url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(encodeNdjson(lines), "application/x-ndjson"), client.execute, Effect.asVoid, Effect.mapError((error) => error.response !== void 0 ? new ProductEventsPostError({
494
- message: `POST ${resolved.url} → ${error.response.status}`,
495
- status: error.response.status
496
- }) : new ProductEventsPostError({
497
- cause: error,
498
- message: `POST ${resolved.url} failed: ${error.message}`
499
- })));
500
- };
501
- const flush = flushing.withPermits(1)(Effect.suspend(() => {
502
- if (buffer.length === 0) return Effect.void;
503
- const batch = buffer;
504
- buffer = [];
505
- if (resolved.clientId === void 0) {
506
- warnMissingClient();
507
- return Effect.void;
508
- }
509
- return post(batch).pipe(Effect.catchCause((cause) => Effect.sync(() => {
510
- warnPost(Option.getOrElse(Cause.findErrorOption(cause), () => new ProductEventsPostError({
511
- cause: Cause.squash(cause),
512
- message: `POST ${resolved.url} failed`
513
- })));
514
- })));
515
- }));
516
- const track = (name, options = {}) => Effect.sync(() => {
517
- const line = encodeProductEventLine({
518
- name,
519
- options,
520
- serviceName: resolved.serviceName,
521
- source: "server"
522
- });
523
- if (line === void 0) {
524
- warnEmptyName();
525
- return false;
526
- }
527
- buffer.push(line);
528
- if (buffer.length > MAX_BUFFERED_EVENTS) buffer.splice(0, buffer.length - MAX_BUFFERED_EVENTS);
529
- return buffer.length >= maxBatchSize;
530
- }).pipe(Effect.flatMap((full) => full ? Effect.forkDetach(flush) : Effect.void), Effect.asVoid);
531
- const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
532
- if (Duration.toMillis(interval) > 0) yield* Effect.forkScoped(flush.pipe(Effect.schedule(Schedule.spaced(interval))));
533
- yield* Effect.addFinalizer(() => flush);
534
- return {
535
- flush,
536
- track
537
- };
538
- });
539
- const layer = (config = {}) => Layer.effect(MxlEvents, make$1(config)).pipe(Layer.provide(FetchHttpClient.layer));
540
- const makeHandle = (config = {}) => {
541
- const runtime = ManagedRuntime.make(layer(config));
542
- const swallow = (promise) => promise.then(() => void 0, () => void 0);
790
+ }
791
+ if (serviceVersion$1) attributes["service.version"] = serviceVersion$1;
792
+ const commitRevision = resolveCommitRevision();
793
+ if (commitRevision) attributes["deployment.commit_sha"] = commitRevision;
794
+ const headRevision = commitRevision ?? (isCommitSha(serviceVersion$1) ? serviceVersion$1 : void 0);
795
+ if (headRevision) attributes["vcs.ref.head.revision"] = headRevision;
796
+ attributes["vcs.repository.url.full"] = stringOrUndefined(process.env["MXL_VCS_REPOSITORY_URL"]) ?? DEFAULT_VCS_REPOSITORY_URL;
797
+ if (config.serviceNamespace) attributes["service.namespace"] = config.serviceNamespace;
798
+ Object.assign(attributes, envResourceAttributes);
799
+ if (config.attributes) Object.assign(attributes, config.attributes);
543
800
  return {
544
- dispose: () => swallow(runtime.dispose()),
545
- flush: () => swallow(runtime.runPromise(Effect.gen(function* () {
546
- yield* (yield* MxlEvents).flush;
547
- }))),
548
- track: (name, options) => {
549
- swallow(runtime.runPromise(Effect.gen(function* () {
550
- yield* (yield* MxlEvents).track(name, options);
551
- })));
801
+ endpoint: endpoint$1,
802
+ environment: environment$1,
803
+ headers,
804
+ resource: {
805
+ serviceName,
806
+ serviceVersion: serviceVersion$1,
807
+ attributes
552
808
  }
553
809
  };
554
- };
555
- //#endregion
556
- //#region src/server/flushable.ts
810
+ }).pipe(Effect.orDie);
811
+ // src/server/layer.ts
812
+ var layer_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
813
+ /**
814
+ * Create an Effect Layer that provides OpenTelemetry traces, logs, and metrics
815
+ * configured for MXL's OTLP collector.
816
+ *
817
+ * Returns a no-op layer when `OTEL_SDK_DISABLED=true`.
818
+ *
819
+ * Wraps the OTLP tracer with `otelGlobalContextBridgeLayer` via `provideMerge`.
820
+ * Measured on effect@4.0.6: `Otlp.layerJson({ tracerContext })` still
821
+ * types the hook as `(primitive, span) => X` and Effect returns that result as
822
+ * the evaluation, so the callback cannot call `~effect/Effect/evaluate` without
823
+ * a `fiber`. `Layer.updateService` also cannot wrap a Tracer the OTLP layer
824
+ * already provides. Keep the merge wrap until Effect passes fiber or evaluates
825
+ * after a side-effect-only context hook.
826
+ */
827
+ const layer = (config = {}) => Layer.unwrap(Effect.gen(function* () {
828
+ const resolved = yield* resolveResource({
829
+ ...config,
830
+ sdkType: "server"
831
+ });
832
+ if (!resolved.endpoint) return Layer.empty;
833
+ const otlp = Otlp.layerJson({
834
+ baseUrl: normalizeOtlpBaseUrl(resolved.endpoint),
835
+ headers: resolved.headers,
836
+ resource: resolved.resource,
837
+ maxBatchSize: config.maxBatchSize,
838
+ loggerExportInterval: config.loggerExportInterval,
839
+ metricsExportInterval: config.metricsExportInterval,
840
+ tracerExportInterval: config.tracerExportInterval,
841
+ shutdownTimeout: config.shutdownTimeout
842
+ }).pipe(Layer.provide(FetchHttpClient.layer));
843
+ return otelGlobalContextBridgeLayer.pipe(Layer.provideMerge(otlp));
844
+ }));
845
+ // src/server/flushable.ts
557
846
  var flushable_exports = /* @__PURE__ */ __exportAll({ make: () => make });
558
847
  const make = (config = {}) => {
559
848
  const eventsConfig = {
@@ -567,90 +856,18 @@ const make = (config = {}) => {
567
856
  return {
568
857
  dispose: handle.dispose,
569
858
  flush: handle.flush,
570
- layer: Layer.mergeAll(layer$1(config), layer(eventsConfig)),
859
+ layer: Layer.mergeAll(layer(config), layer$1(eventsConfig)),
571
860
  track: handle.track
572
861
  };
573
862
  };
574
- //#endregion
575
- //#region src/server/http-middleware.ts
863
+ // src/server/http-middleware.ts
576
864
  function shouldDisableHttpTrace(request) {
577
865
  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);
578
866
  }
579
867
  function tracerDisabledWhenLayer(filter = shouldDisableHttpTrace) {
580
868
  return Layer.succeed(HttpMiddleware.TracerDisabledWhen, filter);
581
869
  }
582
- //#endregion
583
- //#region src/server/trace-correlation.ts
584
- const ZERO_TRACE_ID = "00000000000000000000000000000000";
585
- const ZERO_SPAN_ID = "0000000000000000";
586
- /** Parse W3C `traceparent` (`00-{traceId}-{spanId}-{flags}`). */
587
- function parseTraceparent(traceparent) {
588
- if (!traceparent) return {};
589
- const parts = traceparent.trim().split("-");
590
- if (parts.length < 4) return {};
591
- const traceId = parts[1];
592
- const spanId = parts[2];
593
- if (!traceId || !spanId) return {};
594
- if (traceId.length !== 32 || spanId.length !== 16) return {};
595
- if (traceId === ZERO_TRACE_ID || spanId === ZERO_SPAN_ID) return {};
596
- return {
597
- spanId,
598
- traceId
599
- };
600
- }
601
- /**
602
- * Resolve trace/span IDs for log correlation.
603
- *
604
- * Prefers the active OpenTelemetry span (when a host bridge or queue propagation
605
- * is active), then falls back to a W3C `traceparent` carrier.
606
- */
607
- function getEffectTraceCorrelation(options) {
608
- const activeSpan = trace.getSpan(context.active());
609
- if (activeSpan) {
610
- const spanContext = activeSpan.spanContext();
611
- if (spanContext.traceId && spanContext.traceId !== ZERO_TRACE_ID && spanContext.spanId && spanContext.spanId !== ZERO_SPAN_ID) return {
612
- spanId: spanContext.spanId,
613
- traceId: spanContext.traceId
614
- };
615
- }
616
- return parseTraceparent(options?.traceparent);
617
- }
618
- /** Build log annotation fields from active span correlation. */
619
- function traceCorrelationLogAnnotations(options) {
620
- const { traceparent, ...extra } = options ?? {};
621
- const correlation = getEffectTraceCorrelation({ traceparent });
622
- const annotations = { ...extra };
623
- if (correlation.traceId) {
624
- annotations["trace.trace_id"] = correlation.traceId;
625
- annotations["trace_id"] = correlation.traceId;
626
- }
627
- if (correlation.spanId) {
628
- annotations["trace.span_id"] = correlation.spanId;
629
- annotations["span_id"] = correlation.spanId;
630
- }
631
- return annotations;
632
- }
633
- /** Annotate Effect logs with the active span's trace/span IDs. */
634
- function annotateLogsWithActiveSpanCorrelation(program, fields) {
635
- return Effect.gen(function* () {
636
- const { traceparent, ...extra } = fields ?? {};
637
- const annotations = { ...extra };
638
- const currentSpan = yield* Effect.currentSpan.pipe(Effect.option);
639
- if (Option$1.isSome(currentSpan)) {
640
- annotations["trace.trace_id"] = currentSpan.value.traceId;
641
- annotations["trace_id"] = currentSpan.value.traceId;
642
- annotations["trace.span_id"] = currentSpan.value.spanId;
643
- annotations["span_id"] = currentSpan.value.spanId;
644
- } else Object.assign(annotations, traceCorrelationLogAnnotations({
645
- ...traceparent !== void 0 ? { traceparent } : {},
646
- ...extra
647
- }));
648
- if (Object.keys(annotations).length === 0) return yield* program;
649
- return yield* program.pipe(Effect.annotateLogs(annotations));
650
- });
651
- }
652
- //#endregion
653
- //#region src/server/logging.ts
870
+ // src/server/logging.ts
654
871
  const LOG_LEVEL_BY_ENV = {
655
872
  trace: "Debug",
656
873
  debug: "Debug",
@@ -741,9 +958,8 @@ function logAtLevel(level, message, fields) {
741
958
  case "Fatal": return logError(message, fields);
742
959
  }
743
960
  }
744
- //#endregion
745
- //#region src/server/http-tracing.ts
746
- function headerValue$1(value) {
961
+ // src/server/http-tracing.ts
962
+ function headerValue(value) {
747
963
  if (value === void 0) return void 0;
748
964
  return typeof value === "string" ? value : value[0];
749
965
  }
@@ -758,7 +974,7 @@ function spanContextFromTraceparent(traceparent) {
758
974
  };
759
975
  }
760
976
  function resolveTraceparentFromHeaders(headers) {
761
- return headerValue$1(headers["traceparent"]);
977
+ return headerValue(headers["traceparent"]);
762
978
  }
763
979
  function withParentFromTraceparent(headers, program) {
764
980
  const parentSpanContext = spanContextFromTraceparent(resolveTraceparentFromHeaders(headers));
@@ -879,8 +1095,7 @@ function buildQueueTraceCarrierEffect(headers) {
879
1095
  return buildQueueTraceCarrier(headers);
880
1096
  });
881
1097
  }
882
- //#endregion
883
- //#region src/server/jobs.ts
1098
+ // src/server/jobs.ts
884
1099
  function resolveParentSpanContext(options) {
885
1100
  if (options.parentSpanContext !== void 0) return options.parentSpanContext;
886
1101
  return spanContextFromCarrier(options.parentTraceCarrier);
@@ -912,149 +1127,6 @@ function jobWithSpanEffect(options, program) {
912
1127
  function runJobWithSpan(runtime, options, program) {
913
1128
  return runtime.runPromise(jobWithSpanEffect(options, program));
914
1129
  }
915
- //#endregion
916
- //#region src/server/evlog-bridge.ts
917
- function headerValue(headers, name) {
918
- if (!headers) return void 0;
919
- const direct = headers[name];
920
- if (direct !== void 0) return direct;
921
- const lower = headers[name.toLowerCase()];
922
- if (lower !== void 0) return lower;
923
- }
924
- function assignTraceFields(event, traceId, spanId, overwrite) {
925
- if (overwrite || event["trace_id"] === void 0) event["trace_id"] = traceId;
926
- if (overwrite || event["span_id"] === void 0) event["span_id"] = spanId;
927
- if (overwrite || event["traceId"] === void 0) event["traceId"] = traceId;
928
- if (overwrite || event["spanId"] === void 0) event["spanId"] = spanId;
929
- const trace = typeof event["trace"] === "object" && event["trace"] !== null ? event["trace"] : {};
930
- if (overwrite || trace["trace_id"] === void 0) trace["trace_id"] = traceId;
931
- if (overwrite || trace["span_id"] === void 0) trace["span_id"] = spanId;
932
- if (Object.keys(trace).length > 0) event["trace"] = trace;
933
- }
934
- /**
935
- * Enrich evlog wide events with trace/span IDs from OTel global context or W3C headers.
936
- *
937
- * Use `createEffectTraceEnricher` in the MXL evlog enricher chain.
938
- */
939
- function createEffectTraceEnricher(options = {}) {
940
- const overwrite = options.overwrite ?? false;
941
- return (ctx) => {
942
- const traceparent = headerValue(ctx.headers, "traceparent");
943
- const correlation = getEffectTraceCorrelation({ traceparent });
944
- if (correlation.traceId && correlation.spanId) {
945
- assignTraceFields(ctx.event, correlation.traceId, correlation.spanId, overwrite);
946
- return;
947
- }
948
- const parsed = parseTraceparent(traceparent);
949
- if (parsed.traceId && parsed.spanId) assignTraceFields(ctx.event, parsed.traceId, parsed.spanId, overwrite);
950
- };
951
- }
952
- //#endregion
953
- //#region src/shared/dependency-spans.ts
954
- const MAX_FLUSH_SPAN_LINKS = 32;
955
- function dbClientSpanAttributes(dbSystem, operation) {
956
- return {
957
- "db.system": dbSystem,
958
- "db.system.name": dbSystem,
959
- "peer.service": dbSystem,
960
- ...operation ? { "db.operation.name": operation } : {}
961
- };
962
- }
963
- function queueProducerSpanAttributes(destinationName, peerService = "mxl-worker") {
964
- return {
965
- "messaging.destination.name": destinationName,
966
- "messaging.system": "bullmq",
967
- "peer.service": peerService
968
- };
969
- }
970
- function queueConsumerSpanAttributes(destinationName) {
971
- return {
972
- "messaging.destination.name": destinationName,
973
- "messaging.system": "bullmq"
974
- };
975
- }
976
- function kafkaProducerSpanAttributes(topic, peerService = "kafka") {
977
- return {
978
- "messaging.destination.name": topic,
979
- "messaging.system": "kafka",
980
- "peer.service": peerService
981
- };
982
- }
983
- function bufferSpanAttributes(bufferName, operation) {
984
- return {
985
- "buffer.name": bufferName,
986
- "db.operation.name": operation,
987
- "db.system": "redis",
988
- "db.system.name": "redis",
989
- "peer.service": "redis"
990
- };
991
- }
992
- function geoSpanAttributes() {
993
- return {
994
- "db.operation.name": "resolve",
995
- "db.system": "maxmind",
996
- "db.system.name": "maxmind",
997
- "peer.service": "maxmind"
998
- };
999
- }
1000
- function addSpanLinksFromTraceCarriers(carriers) {
1001
- const span = trace.getActiveSpan();
1002
- if (!span) return 0;
1003
- const seen = /* @__PURE__ */ new Set();
1004
- let linked = 0;
1005
- for (const carrier of carriers) {
1006
- if (!carrier.traceparent || seen.has(carrier.traceparent)) continue;
1007
- seen.add(carrier.traceparent);
1008
- const extracted = propagation.extract(context.active(), carrier);
1009
- const linkedContext = trace.getSpanContext(extracted);
1010
- if (!linkedContext) continue;
1011
- span.addLink({ context: linkedContext });
1012
- linked += 1;
1013
- if (linked >= MAX_FLUSH_SPAN_LINKS) break;
1014
- }
1015
- return linked;
1016
- }
1017
- function withDependencySpan(name, attributesOrOptions, program) {
1018
- const options = isWithDependencySpanOptions(attributesOrOptions) ? attributesOrOptions : { attributes: attributesOrOptions };
1019
- const attributes = options.attributes ?? {};
1020
- const kind = options.kind ?? "client";
1021
- return program.pipe(Effect.withSpan(name, {
1022
- attributes,
1023
- kind
1024
- }));
1025
- }
1026
- function isWithDependencySpanOptions(value) {
1027
- return typeof value === "object" && ("kind" in value || "attributes" in value && typeof value.attributes === "object");
1028
- }
1029
- /** Promise-based dependency span for non-Effect code paths. */
1030
- async function withPromiseDependencySpan(name, attributes, operation, kind = "client") {
1031
- const tracer = trace.getTracer("mxl-effect-sdk");
1032
- const spanKind = kind === "producer" ? SpanKind.PRODUCER : kind === "consumer" ? SpanKind.CONSUMER : SpanKind.CLIENT;
1033
- return tracer.startActiveSpan(name, {
1034
- attributes,
1035
- kind: spanKind
1036
- }, async (span) => {
1037
- try {
1038
- const result = await operation();
1039
- span.end();
1040
- return result;
1041
- } catch (error) {
1042
- if (error instanceof Error) {
1043
- span.recordException(error);
1044
- span.setStatus({
1045
- code: SpanStatusCode.ERROR,
1046
- message: error.message
1047
- });
1048
- } else span.setStatus({
1049
- code: SpanStatusCode.ERROR,
1050
- message: String(error)
1051
- });
1052
- span.end();
1053
- throw error;
1054
- }
1055
- });
1056
- }
1057
- //#endregion
1058
- export { minimumLogLevelLayer as A, flushable_exports as B, withParentFromTraceparent as C, logError as D, logDebug as E, getEffectTraceCorrelation as F, parseOtelResourceAttributes as G, layer_exports as H, parseTraceparent as I, buildTraceCarrierFromSpan as J, parseOtlpHeaders 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, derivePlatformAttributes as U, events_exports as V, getAutoPlatformAttributes as W, otelGlobalContextBridgeLayer as X, formatTraceparent as Y, wrapTracerWithOtelContextBridge as Z, 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, normalizeOtlpBaseUrl 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 };
1130
+ export { parseOtlpHeaders as $, normalizeOtlpBaseUrl as A, events_exports as B, serializeLogError as C, layer_exports as D, flushable_exports as E, createEffectTraceEnricher as F, kafkaProducerSpanAttributes as G, bufferSpanAttributes as H, annotateLogsWithActiveSpanCorrelation as I, withDependencySpan as J, queueConsumerSpanAttributes as K, getEffectTraceCorrelation as L, formatTraceparent as M, otelGlobalContextBridgeLayer as N, derivePlatformAttributes as O, wrapTracerWithOtelContextBridge as P, parseOtelResourceAttributes as Q, parseTraceparent as R, resolveLogLevelFromEnv as S, tracerDisabledWhenLayer as T, dbClientSpanAttributes as U, addSpanLinksFromTraceCarriers as V, geoSpanAttributes as W, deriveContainerAttributes as X, withPromiseDependencySpan as Y, getContainerAttributes as Z, logError as _, finalizeHttpSpan as a, minimumLogLevelLayer as b, resolveTraceparentFromHeaders as c, withHttpServerSpan as d, failureIdentifier as et, withParentFromTraceCarrier as f, logDebug as g, logAtLevel as h, buildQueueTraceCarrierEffect as i, buildTraceCarrierFromSpan as j, getAutoPlatformAttributes as k, spanContextFromCarrier as l, buildHttpStatusLogFields as m, runJobWithSpan as n, logHttpRequestFailure as o, withParentFromTraceparent as p, queueProducerSpanAttributes as q, buildQueueTraceCarrier as r, logHttpResponse as s, jobWithSpanEffect as t, resolveSpanFilter as tt, spanContextFromTraceparent as u, logInfo as v, shouldDisableHttpTrace as w, resolveEffectLogLevelFromHttpStatus as x, logWarning as y, traceCorrelationLogAnnotations as z };
1059
1131
 
1060
- //# sourceMappingURL=server-25vCyy-w.mjs.map
1132
+ //# sourceMappingURL=server-BMDPjez-.mjs.map