@safaricom-mxl/effect-sdk 0.0.22 → 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 +83 -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-BoMrxG4x.mjs → server-BMDPjez-.mjs} +645 -565
- 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-BoMrxG4x.mjs.map +0 -1
|
@@ -1,101 +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
|
-
|
|
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
|
-
* Put Effect span ids on the global OpenTelemetry context.
|
|
44
|
-
*
|
|
45
|
-
* Id-only `setSpanContext`. Live `Otel.Span` objects come from
|
|
46
|
-
* `OtelTracer.currentOtelSpan` in Effect space, not from a private TypeId.
|
|
47
|
-
*/
|
|
48
|
-
function effectSpanToOtelContext(activeContext, span) {
|
|
49
|
-
return Otel.trace.setSpanContext(activeContext, {
|
|
50
|
-
isRemote: span._tag === "ExternalSpan",
|
|
51
|
-
spanId: span.spanId,
|
|
52
|
-
traceFlags: span.sampled ? Otel.TraceFlags.SAMPLED : Otel.TraceFlags.NONE,
|
|
53
|
-
traceId: span.traceId
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Wrap a tracer so Effect spans activate global `@opentelemetry/api` context.
|
|
58
|
-
*
|
|
59
|
-
* Prefer this over `Otlp.layerJson({ tracerContext })` until Effect changes
|
|
60
|
-
* that hook. On effect@4.0.2 the callback is still `(primitive, span)`
|
|
61
|
-
* and must return the evaluation, but `EffectPrimitive` only evaluates with a
|
|
62
|
-
* `fiber`. Keep this wrap until Effect passes fiber or evaluates after the hook.
|
|
63
|
-
*/
|
|
64
|
-
function wrapTracerWithOtelContextBridge(base) {
|
|
65
|
-
ensureOtelContextManager();
|
|
66
|
-
const existingContext = base.context;
|
|
67
|
-
return Tracer.make({
|
|
68
|
-
span(options) {
|
|
69
|
-
return base.span(options);
|
|
70
|
-
},
|
|
71
|
-
context(primitive, fiber) {
|
|
72
|
-
if (fiber.cache.span === void 0) {
|
|
73
|
-
if (existingContext) return existingContext(primitive, fiber);
|
|
74
|
-
return primitive[evaluate](fiber);
|
|
75
|
-
}
|
|
76
|
-
return Otel.context.with(effectSpanToOtelContext(Otel.context.active(), fiber.cache.span), () => {
|
|
77
|
-
if (existingContext) return existingContext(primitive, fiber);
|
|
78
|
-
return primitive[evaluate](fiber);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Layer that wraps the configured Effect tracer with a global OTel context bridge.
|
|
85
|
-
*
|
|
86
|
-
* Must be provided over `Otlp.layerJson` so queue propagation, log correlation,
|
|
87
|
-
* and outbound W3C headers see the active server span.
|
|
88
|
-
*/
|
|
89
|
-
const otelGlobalContextBridgeLayer = Layer.effect(Tracer.Tracer, Effect.gen(function* () {
|
|
90
|
-
return wrapTracerWithOtelContextBridge(yield* Tracer.Tracer);
|
|
91
|
-
}));
|
|
92
|
-
//#endregion
|
|
93
|
-
//#region src/server/otlp.ts
|
|
94
|
-
function normalizeOtlpBaseUrl(url) {
|
|
95
|
-
const trimmed = url.replace(/\/$/, "");
|
|
96
|
-
if (trimmed.endsWith("/v1/traces") || trimmed.endsWith("/v1/logs")) return trimmed.replace(/\/v1\/(traces|logs|metrics)$/, "");
|
|
97
|
-
return trimmed;
|
|
98
|
-
}
|
|
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
|
+
};
|
|
99
43
|
/** Resolve the OTLP exporter endpoint from standard env vars. */
|
|
100
44
|
const endpoint = Effect.gen(function* () {
|
|
101
45
|
const otlp = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_ENDPOINT"));
|
|
@@ -147,36 +91,547 @@ const otelResourceAttributes = Effect.gen(function* () {
|
|
|
147
91
|
onNone: () => ({}),
|
|
148
92
|
onSome: (value) => parseOtelResourceAttributes(value)
|
|
149
93
|
});
|
|
150
|
-
});
|
|
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
|
+
}
|
|
151
592
|
/**
|
|
152
|
-
*
|
|
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.
|
|
153
599
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
});
|
|
166
616
|
}
|
|
167
|
-
}
|
|
168
|
-
return result;
|
|
169
|
-
};
|
|
170
|
-
/** Resolve OTLP export headers from env. */
|
|
171
|
-
const otlpHeaders = Effect.gen(function* () {
|
|
172
|
-
const raw = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_HEADERS"));
|
|
173
|
-
return Option.match(raw, {
|
|
174
|
-
onNone: () => ({}),
|
|
175
|
-
onSome: (value) => parseOtlpHeaders(value)
|
|
176
617
|
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
180
635
|
const empty = {};
|
|
181
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));
|
|
182
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));
|
|
@@ -288,15 +743,20 @@ const getAutoPlatformAttributes = () => {
|
|
|
288
743
|
env
|
|
289
744
|
});
|
|
290
745
|
};
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
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;
|
|
294
748
|
const DEFAULT_VCS_REPOSITORY_URL = "https://gitlab.safaricom.co.ke/mxl/apm/mxl-effect-sdk";
|
|
295
749
|
function resolveCommitRevision() {
|
|
296
|
-
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"]);
|
|
297
751
|
}
|
|
298
752
|
let serviceInstanceId;
|
|
299
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);
|
|
300
760
|
const resolveResource = (config) => Effect.gen(function* () {
|
|
301
761
|
if (yield* sdkDisabled) return {
|
|
302
762
|
endpoint: void 0,
|
|
@@ -321,231 +781,68 @@ const resolveResource = (config) => Effect.gen(function* () {
|
|
|
321
781
|
const envResourceAttributes = yield* otelResourceAttributes;
|
|
322
782
|
const attributes = {};
|
|
323
783
|
Object.assign(attributes, getAutoPlatformAttributes());
|
|
784
|
+
Object.assign(attributes, getContainerAttributes());
|
|
324
785
|
attributes["mxl.sdk.type"] = config.sdkType ?? "server";
|
|
325
786
|
attributes["service.instance.id"] = getServiceInstanceId();
|
|
326
787
|
if (environment$1) {
|
|
327
788
|
attributes["deployment.environment"] = environment$1;
|
|
328
789
|
attributes["deployment.environment.name"] = environment$1;
|
|
329
|
-
}
|
|
330
|
-
if (serviceVersion$1) attributes["service.version"] = serviceVersion$1;
|
|
331
|
-
const commitRevision = resolveCommitRevision();
|
|
332
|
-
if (commitRevision)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
attributes["
|
|
337
|
-
Object.assign(attributes, envResourceAttributes);
|
|
338
|
-
if (config.attributes) Object.assign(attributes, config.attributes);
|
|
339
|
-
return {
|
|
340
|
-
endpoint: endpoint$1,
|
|
341
|
-
environment: environment$1,
|
|
342
|
-
headers,
|
|
343
|
-
resource: {
|
|
344
|
-
serviceName,
|
|
345
|
-
serviceVersion: serviceVersion$1,
|
|
346
|
-
attributes
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
}).pipe(Effect.orDie);
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region src/server/layer.ts
|
|
352
|
-
var layer_exports = /* @__PURE__ */ __exportAll({ layer: () => layer$1 });
|
|
353
|
-
/**
|
|
354
|
-
* Create an Effect Layer that provides OpenTelemetry traces, logs, and metrics
|
|
355
|
-
* configured for MXL's OTLP collector.
|
|
356
|
-
*
|
|
357
|
-
* Returns a no-op layer when `OTEL_SDK_DISABLED=true`.
|
|
358
|
-
*
|
|
359
|
-
* Wraps the OTLP tracer with `otelGlobalContextBridgeLayer` via `provideMerge`.
|
|
360
|
-
* Measured on effect@4.0.2: `Otlp.layerJson({ tracerContext })` still
|
|
361
|
-
* types the hook as `(primitive, span) => X` and Effect returns that result as
|
|
362
|
-
* the evaluation, so the callback cannot call `~effect/Effect/evaluate` without
|
|
363
|
-
* a `fiber`. `Layer.updateService` also cannot wrap a Tracer the OTLP layer
|
|
364
|
-
* already provides. Keep the merge wrap until Effect passes fiber or evaluates
|
|
365
|
-
* after a side-effect-only context hook.
|
|
366
|
-
*/
|
|
367
|
-
const layer$1 = (config = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
368
|
-
const resolved = yield* resolveResource({
|
|
369
|
-
...config,
|
|
370
|
-
sdkType: "server"
|
|
371
|
-
});
|
|
372
|
-
if (!resolved.endpoint) return Layer.empty;
|
|
373
|
-
const otlp = Otlp.layerJson({
|
|
374
|
-
baseUrl: normalizeOtlpBaseUrl(resolved.endpoint),
|
|
375
|
-
headers: resolved.headers,
|
|
376
|
-
resource: resolved.resource,
|
|
377
|
-
maxBatchSize: config.maxBatchSize,
|
|
378
|
-
loggerExportInterval: config.loggerExportInterval,
|
|
379
|
-
metricsExportInterval: config.metricsExportInterval,
|
|
380
|
-
tracerExportInterval: config.tracerExportInterval,
|
|
381
|
-
shutdownTimeout: config.shutdownTimeout
|
|
382
|
-
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
383
|
-
return otelGlobalContextBridgeLayer.pipe(Layer.provideMerge(otlp));
|
|
384
|
-
}));
|
|
385
|
-
const RESERVED_EVENT_NAMES = ["session_start", "session_end"];
|
|
386
|
-
const isReservedEventName = (name) => RESERVED_EVENT_NAMES.some((reserved) => reserved === name);
|
|
387
|
-
const coerceTrackProps = (props) => {
|
|
388
|
-
if (props === void 0) return {};
|
|
389
|
-
const attributes = {};
|
|
390
|
-
for (const [key, value] of Object.entries(props)) {
|
|
391
|
-
if (value === void 0) continue;
|
|
392
|
-
attributes[key] = typeof value === "string" ? value : String(value);
|
|
393
|
-
}
|
|
394
|
-
return attributes;
|
|
395
|
-
};
|
|
396
|
-
const toEventTimestamp = (value) => (value instanceof Date ? value : new Date(value ?? Date.now())).toISOString();
|
|
397
|
-
const encodeProductEventLine = (input) => {
|
|
398
|
-
const trimmed = typeof input.name === "string" ? input.name.trim() : "";
|
|
399
|
-
if (trimmed.length === 0 || isReservedEventName(trimmed)) return;
|
|
400
|
-
const options = input.options ?? {};
|
|
401
|
-
return {
|
|
402
|
-
attributes: coerceTrackProps(options.attributes),
|
|
403
|
-
group_id: options.groupId ?? "",
|
|
404
|
-
name: trimmed.slice(0, 80),
|
|
405
|
-
page_path: options.pagePath ?? "",
|
|
406
|
-
service_name: input.serviceName,
|
|
407
|
-
session_id: options.sessionId ?? "",
|
|
408
|
-
source: input.source,
|
|
409
|
-
timestamp: toEventTimestamp(options.timestamp),
|
|
410
|
-
traceparent: options.traceparent,
|
|
411
|
-
url: options.url ?? "",
|
|
412
|
-
user_id: options.userId ?? "",
|
|
413
|
-
visitor_id: options.visitorId ?? ""
|
|
414
|
-
};
|
|
415
|
-
};
|
|
416
|
-
const encodeNdjson = (lines) => `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`;
|
|
417
|
-
//#endregion
|
|
418
|
-
//#region src/version.ts
|
|
419
|
-
const SDK_NAME = "mxl-effect-sdk";
|
|
420
|
-
const SDK_VERSION = "0.0.15";
|
|
421
|
-
//#endregion
|
|
422
|
-
//#region src/server/events.ts
|
|
423
|
-
var events_exports = /* @__PURE__ */ __exportAll({
|
|
424
|
-
MxlEvents: () => MxlEvents,
|
|
425
|
-
ProductEventsPostError: () => ProductEventsPostError,
|
|
426
|
-
layer: () => layer,
|
|
427
|
-
make: () => make$1,
|
|
428
|
-
makeHandle: () => makeHandle
|
|
429
|
-
});
|
|
430
|
-
const DEFAULT_FLUSH_INTERVAL = Duration.seconds(5);
|
|
431
|
-
const DEFAULT_MAX_BATCH_SIZE = 100;
|
|
432
|
-
const MAX_BUFFERED_EVENTS = 1e3;
|
|
433
|
-
const WARN_INTERVAL_MS = 3e4;
|
|
434
|
-
const DEFAULT_BASKET_URL = "http://127.0.0.1:8000";
|
|
435
|
-
var MxlEvents = class extends Context.Service()("@safaricom-mxl/effect-sdk/MxlEvents") {};
|
|
436
|
-
var ProductEventsPostError = class extends Schema.TaggedError()("@safaricom-mxl/effect-sdk/ProductEventsPostError", {
|
|
437
|
-
cause: Schema.optionalKey(Schema.Defect()),
|
|
438
|
-
message: Schema.String,
|
|
439
|
-
status: Schema.optionalKey(Schema.Number)
|
|
440
|
-
}) {};
|
|
441
|
-
SchemaJITCompiler.enable(ProductEventsPostError.ast);
|
|
442
|
-
const stringOrUndefined = (value) => value && value.length > 0 ? value : void 0;
|
|
443
|
-
const resolveEventsConfig = (config) => {
|
|
444
|
-
const endpoint = (config.endpoint ?? process.env["MXL_BASKET_URL"] ?? DEFAULT_BASKET_URL).replace(/\/$/, "");
|
|
445
|
-
return {
|
|
446
|
-
apiKey: stringOrUndefined(config.apiKey ?? process.env["MXL_API_KEY"]),
|
|
447
|
-
clientId: stringOrUndefined(config.clientId ?? process.env["MXL_CLIENT_ID"]),
|
|
448
|
-
serviceName: config.serviceName ?? process.env["OTEL_SERVICE_NAME"] ?? "unknown",
|
|
449
|
-
url: `${endpoint}${config.eventsPath ?? "/events"}`
|
|
450
|
-
};
|
|
451
|
-
};
|
|
452
|
-
const make$1 = Effect.fn("MxlEvents.make")(function* (config = {}) {
|
|
453
|
-
const resolved = resolveEventsConfig(config);
|
|
454
|
-
const maxBatchSize = Math.max(1, config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE);
|
|
455
|
-
const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
|
|
456
|
-
const flushing = yield* Semaphore.make(1);
|
|
457
|
-
let buffer = [];
|
|
458
|
-
let warnedAboutName = false;
|
|
459
|
-
let warnedAboutClient = false;
|
|
460
|
-
let lastPostWarnAt = 0;
|
|
461
|
-
const warnPost = (error) => {
|
|
462
|
-
const now = Date.now();
|
|
463
|
-
if (now - lastPostWarnAt < WARN_INTERVAL_MS) return;
|
|
464
|
-
lastPostWarnAt = now;
|
|
465
|
-
console.warn(`[MxlEvents] ${error.message} (dropping batch)`, error.cause ?? "");
|
|
466
|
-
};
|
|
467
|
-
const warnMissingClient = () => {
|
|
468
|
-
if (warnedAboutClient) return;
|
|
469
|
-
warnedAboutClient = true;
|
|
470
|
-
console.warn("[MxlEvents] no client id — set MXL_CLIENT_ID or pass `clientId`; dropping events");
|
|
471
|
-
};
|
|
472
|
-
const warnEmptyName = () => {
|
|
473
|
-
if (warnedAboutName) return;
|
|
474
|
-
warnedAboutName = true;
|
|
475
|
-
console.warn("[MxlEvents] track() needs a non-empty event name; the call was ignored.");
|
|
476
|
-
};
|
|
477
|
-
const post = (lines) => {
|
|
478
|
-
const headers = {
|
|
479
|
-
"mxl-client-id": resolved.clientId ?? "",
|
|
480
|
-
"mxl-sdk-name": SDK_NAME,
|
|
481
|
-
"mxl-sdk-version": SDK_VERSION,
|
|
482
|
-
"user-agent": `${SDK_NAME}-server/${SDK_VERSION}`
|
|
483
|
-
};
|
|
484
|
-
if (resolved.apiKey) headers["mxl-api-key"] = resolved.apiKey;
|
|
485
|
-
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({
|
|
486
|
-
message: `POST ${resolved.url} → ${error.response.status}`,
|
|
487
|
-
status: error.response.status
|
|
488
|
-
}) : new ProductEventsPostError({
|
|
489
|
-
cause: error,
|
|
490
|
-
message: `POST ${resolved.url} failed: ${error.message}`
|
|
491
|
-
})));
|
|
492
|
-
};
|
|
493
|
-
const flush = flushing.withPermits(1)(Effect.suspend(() => {
|
|
494
|
-
if (buffer.length === 0) return Effect.void;
|
|
495
|
-
const batch = buffer;
|
|
496
|
-
buffer = [];
|
|
497
|
-
if (resolved.clientId === void 0) {
|
|
498
|
-
warnMissingClient();
|
|
499
|
-
return Effect.void;
|
|
500
|
-
}
|
|
501
|
-
return post(batch).pipe(Effect.catchCause((cause) => Effect.sync(() => {
|
|
502
|
-
warnPost(Option.getOrElse(Cause.findErrorOption(cause), () => new ProductEventsPostError({
|
|
503
|
-
cause: Cause.squash(cause),
|
|
504
|
-
message: `POST ${resolved.url} failed`
|
|
505
|
-
})));
|
|
506
|
-
})));
|
|
507
|
-
}));
|
|
508
|
-
const track = (name, options = {}) => Effect.sync(() => {
|
|
509
|
-
const line = encodeProductEventLine({
|
|
510
|
-
name,
|
|
511
|
-
options,
|
|
512
|
-
serviceName: resolved.serviceName,
|
|
513
|
-
source: "server"
|
|
514
|
-
});
|
|
515
|
-
if (line === void 0) {
|
|
516
|
-
warnEmptyName();
|
|
517
|
-
return false;
|
|
518
|
-
}
|
|
519
|
-
buffer.push(line);
|
|
520
|
-
if (buffer.length > MAX_BUFFERED_EVENTS) buffer.splice(0, buffer.length - MAX_BUFFERED_EVENTS);
|
|
521
|
-
return buffer.length >= maxBatchSize;
|
|
522
|
-
}).pipe(Effect.flatMap((full) => full ? Effect.forkDetach(flush) : Effect.void), Effect.asVoid);
|
|
523
|
-
const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
524
|
-
if (Duration.toMillis(interval) > 0) yield* Effect.forkScoped(flush.pipe(Effect.schedule(Schedule.spaced(interval))));
|
|
525
|
-
yield* Effect.addFinalizer(() => flush);
|
|
526
|
-
return {
|
|
527
|
-
flush,
|
|
528
|
-
track
|
|
529
|
-
};
|
|
530
|
-
});
|
|
531
|
-
const layer = (config = {}) => Layer.effect(MxlEvents, make$1(config)).pipe(Layer.provide(FetchHttpClient.layer));
|
|
532
|
-
const makeHandle = (config = {}) => {
|
|
533
|
-
const runtime = ManagedRuntime.make(layer(config));
|
|
534
|
-
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);
|
|
535
800
|
return {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
})));
|
|
801
|
+
endpoint: endpoint$1,
|
|
802
|
+
environment: environment$1,
|
|
803
|
+
headers,
|
|
804
|
+
resource: {
|
|
805
|
+
serviceName,
|
|
806
|
+
serviceVersion: serviceVersion$1,
|
|
807
|
+
attributes
|
|
544
808
|
}
|
|
545
809
|
};
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
549
846
|
var flushable_exports = /* @__PURE__ */ __exportAll({ make: () => make });
|
|
550
847
|
const make = (config = {}) => {
|
|
551
848
|
const eventsConfig = {
|
|
@@ -559,90 +856,18 @@ const make = (config = {}) => {
|
|
|
559
856
|
return {
|
|
560
857
|
dispose: handle.dispose,
|
|
561
858
|
flush: handle.flush,
|
|
562
|
-
layer: Layer.mergeAll(layer
|
|
859
|
+
layer: Layer.mergeAll(layer(config), layer$1(eventsConfig)),
|
|
563
860
|
track: handle.track
|
|
564
861
|
};
|
|
565
862
|
};
|
|
566
|
-
|
|
567
|
-
//#region src/server/http-middleware.ts
|
|
863
|
+
// src/server/http-middleware.ts
|
|
568
864
|
function shouldDisableHttpTrace(request) {
|
|
569
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);
|
|
570
866
|
}
|
|
571
867
|
function tracerDisabledWhenLayer(filter = shouldDisableHttpTrace) {
|
|
572
868
|
return Layer.succeed(HttpMiddleware.TracerDisabledWhen, filter);
|
|
573
869
|
}
|
|
574
|
-
|
|
575
|
-
//#region src/server/trace-correlation.ts
|
|
576
|
-
const ZERO_TRACE_ID = "00000000000000000000000000000000";
|
|
577
|
-
const ZERO_SPAN_ID = "0000000000000000";
|
|
578
|
-
/** Parse W3C `traceparent` (`00-{traceId}-{spanId}-{flags}`). */
|
|
579
|
-
function parseTraceparent(traceparent) {
|
|
580
|
-
if (!traceparent) return {};
|
|
581
|
-
const parts = traceparent.trim().split("-");
|
|
582
|
-
if (parts.length < 4) return {};
|
|
583
|
-
const traceId = parts[1];
|
|
584
|
-
const spanId = parts[2];
|
|
585
|
-
if (!traceId || !spanId) return {};
|
|
586
|
-
if (traceId.length !== 32 || spanId.length !== 16) return {};
|
|
587
|
-
if (traceId === ZERO_TRACE_ID || spanId === ZERO_SPAN_ID) return {};
|
|
588
|
-
return {
|
|
589
|
-
spanId,
|
|
590
|
-
traceId
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* Resolve trace/span IDs for log correlation.
|
|
595
|
-
*
|
|
596
|
-
* Prefers the active OpenTelemetry span (when a host bridge or queue propagation
|
|
597
|
-
* is active), then falls back to a W3C `traceparent` carrier.
|
|
598
|
-
*/
|
|
599
|
-
function getEffectTraceCorrelation(options) {
|
|
600
|
-
const activeSpan = trace.getSpan(context.active());
|
|
601
|
-
if (activeSpan) {
|
|
602
|
-
const spanContext = activeSpan.spanContext();
|
|
603
|
-
if (spanContext.traceId && spanContext.traceId !== ZERO_TRACE_ID && spanContext.spanId && spanContext.spanId !== ZERO_SPAN_ID) return {
|
|
604
|
-
spanId: spanContext.spanId,
|
|
605
|
-
traceId: spanContext.traceId
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
return parseTraceparent(options?.traceparent);
|
|
609
|
-
}
|
|
610
|
-
/** Build log annotation fields from active span correlation. */
|
|
611
|
-
function traceCorrelationLogAnnotations(options) {
|
|
612
|
-
const { traceparent, ...extra } = options ?? {};
|
|
613
|
-
const correlation = getEffectTraceCorrelation({ traceparent });
|
|
614
|
-
const annotations = { ...extra };
|
|
615
|
-
if (correlation.traceId) {
|
|
616
|
-
annotations["trace.trace_id"] = correlation.traceId;
|
|
617
|
-
annotations["trace_id"] = correlation.traceId;
|
|
618
|
-
}
|
|
619
|
-
if (correlation.spanId) {
|
|
620
|
-
annotations["trace.span_id"] = correlation.spanId;
|
|
621
|
-
annotations["span_id"] = correlation.spanId;
|
|
622
|
-
}
|
|
623
|
-
return annotations;
|
|
624
|
-
}
|
|
625
|
-
/** Annotate Effect logs with the active span's trace/span IDs. */
|
|
626
|
-
function annotateLogsWithActiveSpanCorrelation(program, fields) {
|
|
627
|
-
return Effect.gen(function* () {
|
|
628
|
-
const { traceparent, ...extra } = fields ?? {};
|
|
629
|
-
const annotations = { ...extra };
|
|
630
|
-
const currentSpan = yield* Effect.currentSpan.pipe(Effect.option);
|
|
631
|
-
if (Option$1.isSome(currentSpan)) {
|
|
632
|
-
annotations["trace.trace_id"] = currentSpan.value.traceId;
|
|
633
|
-
annotations["trace_id"] = currentSpan.value.traceId;
|
|
634
|
-
annotations["trace.span_id"] = currentSpan.value.spanId;
|
|
635
|
-
annotations["span_id"] = currentSpan.value.spanId;
|
|
636
|
-
} else Object.assign(annotations, traceCorrelationLogAnnotations({
|
|
637
|
-
...traceparent !== void 0 ? { traceparent } : {},
|
|
638
|
-
...extra
|
|
639
|
-
}));
|
|
640
|
-
if (Object.keys(annotations).length === 0) return yield* program;
|
|
641
|
-
return yield* program.pipe(Effect.annotateLogs(annotations));
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
//#endregion
|
|
645
|
-
//#region src/server/logging.ts
|
|
870
|
+
// src/server/logging.ts
|
|
646
871
|
const LOG_LEVEL_BY_ENV = {
|
|
647
872
|
trace: "Debug",
|
|
648
873
|
debug: "Debug",
|
|
@@ -733,9 +958,8 @@ function logAtLevel(level, message, fields) {
|
|
|
733
958
|
case "Fatal": return logError(message, fields);
|
|
734
959
|
}
|
|
735
960
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
function headerValue$1(value) {
|
|
961
|
+
// src/server/http-tracing.ts
|
|
962
|
+
function headerValue(value) {
|
|
739
963
|
if (value === void 0) return void 0;
|
|
740
964
|
return typeof value === "string" ? value : value[0];
|
|
741
965
|
}
|
|
@@ -750,7 +974,7 @@ function spanContextFromTraceparent(traceparent) {
|
|
|
750
974
|
};
|
|
751
975
|
}
|
|
752
976
|
function resolveTraceparentFromHeaders(headers) {
|
|
753
|
-
return headerValue
|
|
977
|
+
return headerValue(headers["traceparent"]);
|
|
754
978
|
}
|
|
755
979
|
function withParentFromTraceparent(headers, program) {
|
|
756
980
|
const parentSpanContext = spanContextFromTraceparent(resolveTraceparentFromHeaders(headers));
|
|
@@ -871,8 +1095,7 @@ function buildQueueTraceCarrierEffect(headers) {
|
|
|
871
1095
|
return buildQueueTraceCarrier(headers);
|
|
872
1096
|
});
|
|
873
1097
|
}
|
|
874
|
-
|
|
875
|
-
//#region src/server/jobs.ts
|
|
1098
|
+
// src/server/jobs.ts
|
|
876
1099
|
function resolveParentSpanContext(options) {
|
|
877
1100
|
if (options.parentSpanContext !== void 0) return options.parentSpanContext;
|
|
878
1101
|
return spanContextFromCarrier(options.parentTraceCarrier);
|
|
@@ -904,149 +1127,6 @@ function jobWithSpanEffect(options, program) {
|
|
|
904
1127
|
function runJobWithSpan(runtime, options, program) {
|
|
905
1128
|
return runtime.runPromise(jobWithSpanEffect(options, program));
|
|
906
1129
|
}
|
|
907
|
-
|
|
908
|
-
//#region src/server/evlog-bridge.ts
|
|
909
|
-
function headerValue(headers, name) {
|
|
910
|
-
if (!headers) return void 0;
|
|
911
|
-
const direct = headers[name];
|
|
912
|
-
if (direct !== void 0) return direct;
|
|
913
|
-
const lower = headers[name.toLowerCase()];
|
|
914
|
-
if (lower !== void 0) return lower;
|
|
915
|
-
}
|
|
916
|
-
function assignTraceFields(event, traceId, spanId, overwrite) {
|
|
917
|
-
if (overwrite || event["trace_id"] === void 0) event["trace_id"] = traceId;
|
|
918
|
-
if (overwrite || event["span_id"] === void 0) event["span_id"] = spanId;
|
|
919
|
-
if (overwrite || event["traceId"] === void 0) event["traceId"] = traceId;
|
|
920
|
-
if (overwrite || event["spanId"] === void 0) event["spanId"] = spanId;
|
|
921
|
-
const trace = typeof event["trace"] === "object" && event["trace"] !== null ? event["trace"] : {};
|
|
922
|
-
if (overwrite || trace["trace_id"] === void 0) trace["trace_id"] = traceId;
|
|
923
|
-
if (overwrite || trace["span_id"] === void 0) trace["span_id"] = spanId;
|
|
924
|
-
if (Object.keys(trace).length > 0) event["trace"] = trace;
|
|
925
|
-
}
|
|
926
|
-
/**
|
|
927
|
-
* Enrich evlog wide events with trace/span IDs from OTel global context or W3C headers.
|
|
928
|
-
*
|
|
929
|
-
* Use `createEffectTraceEnricher` in the MXL evlog enricher chain.
|
|
930
|
-
*/
|
|
931
|
-
function createEffectTraceEnricher(options = {}) {
|
|
932
|
-
const overwrite = options.overwrite ?? false;
|
|
933
|
-
return (ctx) => {
|
|
934
|
-
const traceparent = headerValue(ctx.headers, "traceparent");
|
|
935
|
-
const correlation = getEffectTraceCorrelation({ traceparent });
|
|
936
|
-
if (correlation.traceId && correlation.spanId) {
|
|
937
|
-
assignTraceFields(ctx.event, correlation.traceId, correlation.spanId, overwrite);
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
const parsed = parseTraceparent(traceparent);
|
|
941
|
-
if (parsed.traceId && parsed.spanId) assignTraceFields(ctx.event, parsed.traceId, parsed.spanId, overwrite);
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
//#endregion
|
|
945
|
-
//#region src/shared/dependency-spans.ts
|
|
946
|
-
const MAX_FLUSH_SPAN_LINKS = 32;
|
|
947
|
-
function dbClientSpanAttributes(dbSystem, operation) {
|
|
948
|
-
return {
|
|
949
|
-
"db.system": dbSystem,
|
|
950
|
-
"db.system.name": dbSystem,
|
|
951
|
-
"peer.service": dbSystem,
|
|
952
|
-
...operation ? { "db.operation.name": operation } : {}
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
function queueProducerSpanAttributes(destinationName, peerService = "mxl-worker") {
|
|
956
|
-
return {
|
|
957
|
-
"messaging.destination.name": destinationName,
|
|
958
|
-
"messaging.system": "bullmq",
|
|
959
|
-
"peer.service": peerService
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
function queueConsumerSpanAttributes(destinationName) {
|
|
963
|
-
return {
|
|
964
|
-
"messaging.destination.name": destinationName,
|
|
965
|
-
"messaging.system": "bullmq"
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
function kafkaProducerSpanAttributes(topic, peerService = "kafka") {
|
|
969
|
-
return {
|
|
970
|
-
"messaging.destination.name": topic,
|
|
971
|
-
"messaging.system": "kafka",
|
|
972
|
-
"peer.service": peerService
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
function bufferSpanAttributes(bufferName, operation) {
|
|
976
|
-
return {
|
|
977
|
-
"buffer.name": bufferName,
|
|
978
|
-
"db.operation.name": operation,
|
|
979
|
-
"db.system": "redis",
|
|
980
|
-
"db.system.name": "redis",
|
|
981
|
-
"peer.service": "redis"
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
function geoSpanAttributes() {
|
|
985
|
-
return {
|
|
986
|
-
"db.operation.name": "resolve",
|
|
987
|
-
"db.system": "maxmind",
|
|
988
|
-
"db.system.name": "maxmind",
|
|
989
|
-
"peer.service": "maxmind"
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
function addSpanLinksFromTraceCarriers(carriers) {
|
|
993
|
-
const span = trace.getActiveSpan();
|
|
994
|
-
if (!span) return 0;
|
|
995
|
-
const seen = /* @__PURE__ */ new Set();
|
|
996
|
-
let linked = 0;
|
|
997
|
-
for (const carrier of carriers) {
|
|
998
|
-
if (!carrier.traceparent || seen.has(carrier.traceparent)) continue;
|
|
999
|
-
seen.add(carrier.traceparent);
|
|
1000
|
-
const extracted = propagation.extract(context.active(), carrier);
|
|
1001
|
-
const linkedContext = trace.getSpanContext(extracted);
|
|
1002
|
-
if (!linkedContext) continue;
|
|
1003
|
-
span.addLink({ context: linkedContext });
|
|
1004
|
-
linked += 1;
|
|
1005
|
-
if (linked >= MAX_FLUSH_SPAN_LINKS) break;
|
|
1006
|
-
}
|
|
1007
|
-
return linked;
|
|
1008
|
-
}
|
|
1009
|
-
function withDependencySpan(name, attributesOrOptions, program) {
|
|
1010
|
-
const options = isWithDependencySpanOptions(attributesOrOptions) ? attributesOrOptions : { attributes: attributesOrOptions };
|
|
1011
|
-
const attributes = options.attributes ?? {};
|
|
1012
|
-
const kind = options.kind ?? "client";
|
|
1013
|
-
return program.pipe(Effect.withSpan(name, {
|
|
1014
|
-
attributes,
|
|
1015
|
-
kind
|
|
1016
|
-
}));
|
|
1017
|
-
}
|
|
1018
|
-
function isWithDependencySpanOptions(value) {
|
|
1019
|
-
return typeof value === "object" && ("kind" in value || "attributes" in value && typeof value.attributes === "object");
|
|
1020
|
-
}
|
|
1021
|
-
/** Promise-based dependency span for non-Effect code paths. */
|
|
1022
|
-
async function withPromiseDependencySpan(name, attributes, operation, kind = "client") {
|
|
1023
|
-
const tracer = trace.getTracer("mxl-effect-sdk");
|
|
1024
|
-
const spanKind = kind === "producer" ? SpanKind.PRODUCER : kind === "consumer" ? SpanKind.CONSUMER : SpanKind.CLIENT;
|
|
1025
|
-
return tracer.startActiveSpan(name, {
|
|
1026
|
-
attributes,
|
|
1027
|
-
kind: spanKind
|
|
1028
|
-
}, async (span) => {
|
|
1029
|
-
try {
|
|
1030
|
-
const result = await operation();
|
|
1031
|
-
span.end();
|
|
1032
|
-
return result;
|
|
1033
|
-
} catch (error) {
|
|
1034
|
-
if (error instanceof Error) {
|
|
1035
|
-
span.recordException(error);
|
|
1036
|
-
span.setStatus({
|
|
1037
|
-
code: SpanStatusCode.ERROR,
|
|
1038
|
-
message: error.message
|
|
1039
|
-
});
|
|
1040
|
-
} else span.setStatus({
|
|
1041
|
-
code: SpanStatusCode.ERROR,
|
|
1042
|
-
message: String(error)
|
|
1043
|
-
});
|
|
1044
|
-
span.end();
|
|
1045
|
-
throw error;
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
//#endregion
|
|
1050
|
-
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 };
|
|
1051
1131
|
|
|
1052
|
-
//# sourceMappingURL=server-
|
|
1132
|
+
//# sourceMappingURL=server-BMDPjez-.mjs.map
|