@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.
- package/README.md +34 -13
- package/dist/browser/index.d.ts +50 -89
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +4 -512
- package/dist/browser/index.js.map +1 -1
- package/dist/cloudflare/index.d.ts +1 -2
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/index.js +75 -27
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/flushable-CXe-q-GF.js +1068 -0
- package/dist/flushable-CXe-q-GF.js.map +1 -0
- package/dist/flushable-SCVrRJX3.d.ts +143 -0
- package/dist/flushable-SCVrRJX3.d.ts.map +1 -0
- package/dist/foldkit/index.d.ts +65 -0
- package/dist/foldkit/index.d.ts.map +1 -0
- package/dist/foldkit/index.js +495 -0
- package/dist/foldkit/index.js.map +1 -0
- package/dist/{index-CPZhMHLl.d.mts → index-BUoRJxHg.d.mts} +262 -196
- package/dist/index-BUoRJxHg.d.mts.map +1 -0
- package/dist/index.server.d.mts +2 -2
- package/dist/index.server.mjs +1 -2
- package/dist/{rolldown-runtime-D7D4PA-g.js → rolldown-runtime-D8jQpDG7.js} +1 -2
- package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-D8jQpDG7.mjs} +1 -2
- package/dist/server/index.d.mts +2 -2
- package/dist/server/index.mjs +1 -2
- package/dist/{server-25vCyy-w.mjs → server-BMDPjez-.mjs} +645 -573
- package/dist/server-BMDPjez-.mjs.map +1 -0
- package/package.json +61 -53
- package/dist/index-CPZhMHLl.d.mts.map +0 -1
- package/dist/server-25vCyy-w.mjs.map +0 -1
|
@@ -1,109 +1,45 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
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
|
|
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
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
attributes["
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
556
|
-
|
|
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
|
|
859
|
+
layer: Layer.mergeAll(layer(config), layer$1(eventsConfig)),
|
|
571
860
|
track: handle.track
|
|
572
861
|
};
|
|
573
862
|
};
|
|
574
|
-
|
|
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
|
-
|
|
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
|
-
|
|
745
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1132
|
+
//# sourceMappingURL=server-BMDPjez-.mjs.map
|