@safaricom-mxl/effect-sdk 0.0.15 → 0.0.16
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/LICENSE +201 -0
- package/README.md +33 -6
- package/dist/browser/index.d.ts +71 -7
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +470 -8
- package/dist/browser/index.js.map +1 -1
- package/dist/cloudflare/index.d.ts +25 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +547 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/{index-8yBtnSxJ.d.mts → index-BF4-zL-9.d.mts} +71 -6
- package/dist/index-BF4-zL-9.d.mts.map +1 -0
- package/dist/index.server.d.mts +2 -2
- package/dist/index.server.mjs +2 -2
- package/dist/server/index.d.mts +2 -2
- package/dist/server/index.mjs +2 -2
- package/dist/{server-Dz6g7ADZ.mjs → server-DU685moi.mjs} +191 -11
- package/dist/server-DU685moi.mjs.map +1 -0
- package/package.json +34 -21
- package/dist/index-8yBtnSxJ.d.mts.map +0 -1
- package/dist/server-Dz6g7ADZ.mjs.map +0 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Context, Layer } from "effect";
|
|
2
|
+
//#region src/cloudflare/index.d.ts
|
|
3
|
+
export interface Config {
|
|
4
|
+
readonly apiKey?: string | undefined;
|
|
5
|
+
readonly attributes?: Record<string, unknown> | undefined;
|
|
6
|
+
readonly clientId?: string | undefined;
|
|
7
|
+
readonly endpoint?: string | undefined;
|
|
8
|
+
readonly environment?: string | undefined;
|
|
9
|
+
readonly ingestUrl?: string | undefined;
|
|
10
|
+
readonly serviceName?: string | undefined;
|
|
11
|
+
readonly serviceVersion?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
declare const WorkerEnvironment_base: Context.ServiceClass<WorkerEnvironment, "Cloudflare.Workers.WorkerEnvironment", Record<string, unknown>>;
|
|
14
|
+
export declare class WorkerEnvironment extends WorkerEnvironment_base {}
|
|
15
|
+
export interface Telemetry {
|
|
16
|
+
readonly flush: (env: Record<string, unknown>) => Promise<void>;
|
|
17
|
+
readonly layer: Layer.Layer<never>;
|
|
18
|
+
readonly requestLayer: Layer.Layer<never, never, WorkerEnvironment>;
|
|
19
|
+
}
|
|
20
|
+
export declare const make: (config?: Config) => Telemetry;
|
|
21
|
+
export declare const MxlCloudflareSDK: {
|
|
22
|
+
make: typeof make;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cloudflare/index.ts"],"mappings":";;iBAWiB;WACN;WACA,aAAa;WACb;WACA;WACA;WACA;WACA;WACA;;;qBAGE,0BAA0B;iBAKtB;WACN,QAAQ,KAAK,4BAA4B;WACzC,OAAO,MAAM;WACb,cAAc,MAAM,oBAAoB;;qBAwBtC,OAAI,SAAY,WAAc;qBA4D9B;eAAA"}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { Cause, Config, Context, Duration, Effect, Layer, ManagedRuntime, Match, Option, Schedule, Schema, Semaphore } from "effect";
|
|
2
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
import * as Tracer from "effect/Tracer";
|
|
4
|
+
import { Otlp } from "effect/unstable/observability";
|
|
5
|
+
import * as Otel from "@opentelemetry/api";
|
|
6
|
+
import { v7 } from "uuid";
|
|
7
|
+
import { platform, provider, runtime } from "std-env";
|
|
8
|
+
const RESERVED_EVENT_NAMES = ["session_start", "session_end"];
|
|
9
|
+
const isReservedEventName = (name) => RESERVED_EVENT_NAMES.some((reserved) => reserved === name);
|
|
10
|
+
const coerceTrackProps = (props) => {
|
|
11
|
+
if (props === void 0) return {};
|
|
12
|
+
const attributes = {};
|
|
13
|
+
for (const [key, value] of Object.entries(props)) {
|
|
14
|
+
if (value === void 0) continue;
|
|
15
|
+
attributes[key] = typeof value === "string" ? value : String(value);
|
|
16
|
+
}
|
|
17
|
+
return attributes;
|
|
18
|
+
};
|
|
19
|
+
const toEventTimestamp = (value) => (value instanceof Date ? value : new Date(value ?? Date.now())).toISOString();
|
|
20
|
+
const encodeProductEventLine = (input) => {
|
|
21
|
+
const trimmed = typeof input.name === "string" ? input.name.trim() : "";
|
|
22
|
+
if (trimmed.length === 0 || isReservedEventName(trimmed)) return;
|
|
23
|
+
const options = input.options ?? {};
|
|
24
|
+
return {
|
|
25
|
+
attributes: coerceTrackProps(options.attributes),
|
|
26
|
+
group_id: options.groupId ?? "",
|
|
27
|
+
name: trimmed.slice(0, 80),
|
|
28
|
+
page_path: options.pagePath ?? "",
|
|
29
|
+
service_name: input.serviceName,
|
|
30
|
+
session_id: options.sessionId ?? "",
|
|
31
|
+
source: input.source,
|
|
32
|
+
timestamp: toEventTimestamp(options.timestamp),
|
|
33
|
+
traceparent: options.traceparent,
|
|
34
|
+
url: options.url ?? "",
|
|
35
|
+
user_id: options.userId ?? "",
|
|
36
|
+
visitor_id: options.visitorId ?? ""
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const encodeNdjson = (lines) => `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/version.ts
|
|
42
|
+
const SDK_NAME = "mxl-effect-sdk";
|
|
43
|
+
const SDK_VERSION = "0.0.15";
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/server/events.ts
|
|
46
|
+
const DEFAULT_FLUSH_INTERVAL = Duration.seconds(5);
|
|
47
|
+
const DEFAULT_MAX_BATCH_SIZE = 100;
|
|
48
|
+
const MAX_BUFFERED_EVENTS = 1e3;
|
|
49
|
+
const WARN_INTERVAL_MS = 3e4;
|
|
50
|
+
const DEFAULT_BASKET_URL = "http://127.0.0.1:8000";
|
|
51
|
+
var MxlEvents = class extends Context.Service()("@safaricom-mxl/effect-sdk/MxlEvents") {};
|
|
52
|
+
var ProductEventsPostError = class extends Schema.TaggedError()("@safaricom-mxl/effect-sdk/ProductEventsPostError", {
|
|
53
|
+
cause: Schema.optionalKey(Schema.Defect()),
|
|
54
|
+
message: Schema.String,
|
|
55
|
+
status: Schema.optionalKey(Schema.Number)
|
|
56
|
+
}) {};
|
|
57
|
+
const stringOrUndefined$1 = (value) => value && value.length > 0 ? value : void 0;
|
|
58
|
+
const resolveEventsConfig$1 = (config) => {
|
|
59
|
+
const endpoint = (config.endpoint ?? process.env.MXL_BASKET_URL ?? DEFAULT_BASKET_URL).replace(/\/$/, "");
|
|
60
|
+
return {
|
|
61
|
+
apiKey: stringOrUndefined$1(config.apiKey ?? process.env.MXL_API_KEY),
|
|
62
|
+
clientId: stringOrUndefined$1(config.clientId ?? process.env.MXL_CLIENT_ID),
|
|
63
|
+
serviceName: config.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "unknown",
|
|
64
|
+
url: `${endpoint}${config.eventsPath ?? "/events"}`
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const make$1 = Effect.fn("MxlEvents.make")(function* (config = {}) {
|
|
68
|
+
const resolved = resolveEventsConfig$1(config);
|
|
69
|
+
const maxBatchSize = Math.max(1, config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE);
|
|
70
|
+
const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
|
|
71
|
+
const flushing = yield* Semaphore.make(1);
|
|
72
|
+
let buffer = [];
|
|
73
|
+
let warnedAboutName = false;
|
|
74
|
+
let warnedAboutClient = false;
|
|
75
|
+
let lastPostWarnAt = 0;
|
|
76
|
+
const warnPost = (error) => {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (now - lastPostWarnAt < WARN_INTERVAL_MS) return;
|
|
79
|
+
lastPostWarnAt = now;
|
|
80
|
+
console.warn(`[MxlEvents] ${error.message} (dropping batch)`, error.cause ?? "");
|
|
81
|
+
};
|
|
82
|
+
const warnMissingClient = () => {
|
|
83
|
+
if (warnedAboutClient) return;
|
|
84
|
+
warnedAboutClient = true;
|
|
85
|
+
console.warn("[MxlEvents] no client id — set MXL_CLIENT_ID or pass `clientId`; dropping events");
|
|
86
|
+
};
|
|
87
|
+
const warnEmptyName = () => {
|
|
88
|
+
if (warnedAboutName) return;
|
|
89
|
+
warnedAboutName = true;
|
|
90
|
+
console.warn("[MxlEvents] track() needs a non-empty event name; the call was ignored.");
|
|
91
|
+
};
|
|
92
|
+
const post = (lines) => {
|
|
93
|
+
const headers = {
|
|
94
|
+
"mxl-client-id": resolved.clientId ?? "",
|
|
95
|
+
"mxl-sdk-name": SDK_NAME,
|
|
96
|
+
"mxl-sdk-version": SDK_VERSION,
|
|
97
|
+
"user-agent": `${SDK_NAME}-server/${SDK_VERSION}`
|
|
98
|
+
};
|
|
99
|
+
if (resolved.apiKey) headers["mxl-api-key"] = resolved.apiKey;
|
|
100
|
+
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({
|
|
101
|
+
message: `POST ${resolved.url} → ${error.response.status}`,
|
|
102
|
+
status: error.response.status
|
|
103
|
+
}) : new ProductEventsPostError({
|
|
104
|
+
cause: error,
|
|
105
|
+
message: `POST ${resolved.url} failed: ${error.message}`
|
|
106
|
+
})));
|
|
107
|
+
};
|
|
108
|
+
const flush = flushing.withPermits(1)(Effect.suspend(() => {
|
|
109
|
+
if (buffer.length === 0) return Effect.void;
|
|
110
|
+
const batch = buffer;
|
|
111
|
+
buffer = [];
|
|
112
|
+
if (resolved.clientId === void 0) {
|
|
113
|
+
warnMissingClient();
|
|
114
|
+
return Effect.void;
|
|
115
|
+
}
|
|
116
|
+
return post(batch).pipe(Effect.catchCause((cause) => Effect.sync(() => {
|
|
117
|
+
warnPost(Option.getOrElse(Cause.findErrorOption(cause), () => new ProductEventsPostError({
|
|
118
|
+
cause: Cause.squash(cause),
|
|
119
|
+
message: `POST ${resolved.url} failed`
|
|
120
|
+
})));
|
|
121
|
+
})));
|
|
122
|
+
}));
|
|
123
|
+
const track = (name, options = {}) => Effect.sync(() => {
|
|
124
|
+
const line = encodeProductEventLine({
|
|
125
|
+
name,
|
|
126
|
+
options,
|
|
127
|
+
serviceName: resolved.serviceName,
|
|
128
|
+
source: "server"
|
|
129
|
+
});
|
|
130
|
+
if (line === void 0) {
|
|
131
|
+
warnEmptyName();
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
buffer.push(line);
|
|
135
|
+
if (buffer.length > MAX_BUFFERED_EVENTS) buffer.splice(0, buffer.length - MAX_BUFFERED_EVENTS);
|
|
136
|
+
return buffer.length >= maxBatchSize;
|
|
137
|
+
}).pipe(Effect.flatMap((full) => full ? Effect.forkDetach(flush) : Effect.void), Effect.asVoid);
|
|
138
|
+
const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
139
|
+
if (Duration.toMillis(interval) > 0) yield* Effect.forkScoped(flush.pipe(Effect.schedule(Schedule.spaced(interval))));
|
|
140
|
+
yield* Effect.addFinalizer(() => flush);
|
|
141
|
+
return {
|
|
142
|
+
flush,
|
|
143
|
+
track
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
const layer$1 = (config = {}) => Layer.effect(MxlEvents, make$1(config)).pipe(Layer.provide(FetchHttpClient.layer));
|
|
147
|
+
const makeHandle = (config = {}) => {
|
|
148
|
+
const runtime = ManagedRuntime.make(layer$1(config));
|
|
149
|
+
const swallow = (promise) => promise.then(() => void 0, () => void 0);
|
|
150
|
+
return {
|
|
151
|
+
dispose: () => swallow(runtime.dispose()),
|
|
152
|
+
flush: () => swallow(runtime.runPromise(Effect.gen(function* () {
|
|
153
|
+
yield* (yield* MxlEvents).flush;
|
|
154
|
+
}))),
|
|
155
|
+
track: (name, options) => {
|
|
156
|
+
swallow(runtime.runPromise(Effect.gen(function* () {
|
|
157
|
+
yield* (yield* MxlEvents).track(name, options);
|
|
158
|
+
})));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/server/otel-context-bridge.ts
|
|
164
|
+
const OTEL_SPAN_TYPE_ID = "~@effect/opentelemetry/OtelTracer/OtelSpan";
|
|
165
|
+
const evaluate = "~effect/Effect/evaluate";
|
|
166
|
+
/** Map an Effect span to an OpenTelemetry context with the span active. */
|
|
167
|
+
function effectSpanToOtelContext(activeContext, span) {
|
|
168
|
+
if (typeof span === "object" && span !== null && OTEL_SPAN_TYPE_ID in span && "span" in span) return Otel.trace.setSpan(activeContext, span.span);
|
|
169
|
+
return Otel.trace.setSpanContext(activeContext, {
|
|
170
|
+
isRemote: span._tag === "ExternalSpan",
|
|
171
|
+
spanId: span.spanId,
|
|
172
|
+
traceFlags: span.sampled ? Otel.TraceFlags.SAMPLED : Otel.TraceFlags.NONE,
|
|
173
|
+
traceId: span.traceId
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Wrap a tracer so Effect spans activate global `@opentelemetry/api` context. */
|
|
177
|
+
function wrapTracerWithOtelContextBridge(base) {
|
|
178
|
+
const existingContext = base.context;
|
|
179
|
+
return Tracer.make({
|
|
180
|
+
span(options) {
|
|
181
|
+
return base.span(options);
|
|
182
|
+
},
|
|
183
|
+
context(primitive, fiber) {
|
|
184
|
+
if (fiber.cache.span === void 0) {
|
|
185
|
+
if (existingContext) return existingContext(primitive, fiber);
|
|
186
|
+
return primitive[evaluate](fiber);
|
|
187
|
+
}
|
|
188
|
+
return Otel.context.with(effectSpanToOtelContext(Otel.context.active(), fiber.cache.span), () => {
|
|
189
|
+
if (existingContext) return existingContext(primitive, fiber);
|
|
190
|
+
return primitive[evaluate](fiber);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
Layer.effect(Tracer.Tracer, Effect.gen(function* () {
|
|
196
|
+
return wrapTracerWithOtelContextBridge(yield* Tracer.Tracer);
|
|
197
|
+
}));
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/server/otlp.ts
|
|
200
|
+
function normalizeOtlpBaseUrl(url) {
|
|
201
|
+
const trimmed = url.replace(/\/$/, "");
|
|
202
|
+
if (trimmed.endsWith("/v1/traces") || trimmed.endsWith("/v1/logs")) return trimmed.replace(/\/v1\/(traces|logs|metrics)$/, "");
|
|
203
|
+
return trimmed;
|
|
204
|
+
}
|
|
205
|
+
/** Resolve the OTLP exporter endpoint from standard env vars. */
|
|
206
|
+
const endpoint = Effect.gen(function* () {
|
|
207
|
+
const otlp = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_ENDPOINT"));
|
|
208
|
+
if (Option.isSome(otlp)) return otlp;
|
|
209
|
+
return yield* Config.option(Config.String("OTLP_ENDPOINT"));
|
|
210
|
+
});
|
|
211
|
+
const sdkDisabled = Config.Boolean("OTEL_SDK_DISABLED").pipe(Config.withDefault(false));
|
|
212
|
+
/**
|
|
213
|
+
* Resolve service version / commit SHA from platform-specific env vars.
|
|
214
|
+
*
|
|
215
|
+
* Priority: npm_package_version > COMMIT_SHA > RAILWAY_GIT_COMMIT_SHA >
|
|
216
|
+
* VERCEL_GIT_COMMIT_SHA > CF_PAGES_COMMIT_SHA > RENDER_GIT_COMMIT
|
|
217
|
+
*/
|
|
218
|
+
const serviceVersion = Config.option(Config.String("npm_package_version").pipe(Config.orElse(() => Config.String("COMMIT_SHA")), Config.orElse(() => Config.String("RAILWAY_GIT_COMMIT_SHA")), Config.orElse(() => Config.String("VERCEL_GIT_COMMIT_SHA")), Config.orElse(() => Config.String("CF_PAGES_COMMIT_SHA")), Config.orElse(() => Config.String("RENDER_GIT_COMMIT"))));
|
|
219
|
+
/**
|
|
220
|
+
* Resolve deployment environment from env vars.
|
|
221
|
+
*
|
|
222
|
+
* Priority: NODE_ENV > DEPLOYMENT_ENV > "development"
|
|
223
|
+
*/
|
|
224
|
+
const environment = Config.String("NODE_ENV").pipe(Config.orElse(() => Config.String("DEPLOYMENT_ENV")), Config.withDefault("development"));
|
|
225
|
+
/** OTel-standard service name override. */
|
|
226
|
+
const otelServiceName = Config.option(Config.String("OTEL_SERVICE_NAME"));
|
|
227
|
+
/**
|
|
228
|
+
* Parse the OTel-standard `OTEL_RESOURCE_ATTRIBUTES` env var.
|
|
229
|
+
*
|
|
230
|
+
* Format per the OTel spec: comma-separated `key=value` pairs, values may be
|
|
231
|
+
* URL-encoded.
|
|
232
|
+
*/
|
|
233
|
+
const parseOtelResourceAttributes = (input) => {
|
|
234
|
+
const result = {};
|
|
235
|
+
for (const pair of input.split(",")) {
|
|
236
|
+
const eq = pair.indexOf("=");
|
|
237
|
+
if (eq === -1) continue;
|
|
238
|
+
const key = pair.slice(0, eq).trim();
|
|
239
|
+
if (!key) continue;
|
|
240
|
+
const raw = pair.slice(eq + 1).trim();
|
|
241
|
+
let value = raw;
|
|
242
|
+
try {
|
|
243
|
+
value = decodeURIComponent(raw);
|
|
244
|
+
} catch {}
|
|
245
|
+
result[key] = value;
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
};
|
|
249
|
+
/** Resolve and parse `OTEL_RESOURCE_ATTRIBUTES`. */
|
|
250
|
+
const otelResourceAttributes = Effect.gen(function* () {
|
|
251
|
+
const raw = yield* Config.option(Config.String("OTEL_RESOURCE_ATTRIBUTES"));
|
|
252
|
+
return Option.match(raw, {
|
|
253
|
+
onNone: () => ({}),
|
|
254
|
+
onSome: (value) => parseOtelResourceAttributes(value)
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
/**
|
|
258
|
+
* Parse `OTEL_EXPORTER_OTLP_HEADERS` (`key=value,key2=value2`) into a header map.
|
|
259
|
+
*/
|
|
260
|
+
const parseOtlpHeaders = (input) => {
|
|
261
|
+
const result = {};
|
|
262
|
+
for (const pair of input.split(",")) {
|
|
263
|
+
const eq = pair.indexOf("=");
|
|
264
|
+
if (eq === -1) continue;
|
|
265
|
+
const key = pair.slice(0, eq).trim();
|
|
266
|
+
if (!key) continue;
|
|
267
|
+
const raw = pair.slice(eq + 1).trim();
|
|
268
|
+
try {
|
|
269
|
+
result[key] = decodeURIComponent(raw);
|
|
270
|
+
} catch {
|
|
271
|
+
result[key] = raw;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
};
|
|
276
|
+
/** Resolve OTLP export headers from env. */
|
|
277
|
+
const otlpHeaders = Effect.gen(function* () {
|
|
278
|
+
const raw = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_HEADERS"));
|
|
279
|
+
return Option.match(raw, {
|
|
280
|
+
onNone: () => ({}),
|
|
281
|
+
onSome: (value) => parseOtlpHeaders(value)
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/server/platform.ts
|
|
286
|
+
const empty = {};
|
|
287
|
+
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));
|
|
288
|
+
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));
|
|
289
|
+
const archAttrs = (a) => a === "" ? empty : { "host.arch": Match.value(a).pipe(Match.when("x64", () => "amd64"), Match.when("ia32", () => "x86"), Match.orElse((other) => other)) };
|
|
290
|
+
const lambdaAttrs = (env) => ({
|
|
291
|
+
"cloud.provider": "aws",
|
|
292
|
+
"cloud.platform": "aws_lambda",
|
|
293
|
+
"faas.name": env.AWS_LAMBDA_FUNCTION_NAME,
|
|
294
|
+
...env.AWS_LAMBDA_FUNCTION_VERSION && { "faas.version": env.AWS_LAMBDA_FUNCTION_VERSION },
|
|
295
|
+
...env.AWS_LAMBDA_LOG_STREAM_NAME && { "faas.instance": env.AWS_LAMBDA_LOG_STREAM_NAME },
|
|
296
|
+
...env.AWS_REGION ? { "cloud.region": env.AWS_REGION } : env.AWS_DEFAULT_REGION ? { "cloud.region": env.AWS_DEFAULT_REGION } : {}
|
|
297
|
+
});
|
|
298
|
+
const providerAttrs = (prov, env) => Match.value(prov).pipe(Match.when("cloudflare_workers", () => ({
|
|
299
|
+
"cloud.provider": "cloudflare",
|
|
300
|
+
"cloud.platform": "cloudflare.workers"
|
|
301
|
+
})), Match.when("cloudflare_pages", () => ({
|
|
302
|
+
"cloud.provider": "cloudflare",
|
|
303
|
+
"cloud.platform": "cloudflare.pages"
|
|
304
|
+
})), Match.when("vercel", () => ({
|
|
305
|
+
"cloud.provider": "vercel",
|
|
306
|
+
"cloud.platform": "vercel",
|
|
307
|
+
...env.VERCEL_REGION && { "cloud.region": env.VERCEL_REGION },
|
|
308
|
+
...env.VERCEL_DEPLOYMENT_ID && { "faas.instance": env.VERCEL_DEPLOYMENT_ID }
|
|
309
|
+
})), Match.when("netlify", () => ({
|
|
310
|
+
"cloud.provider": "netlify",
|
|
311
|
+
"cloud.platform": "netlify"
|
|
312
|
+
})), Match.whenOr("google_cloudrun", "google_cloudrun_job", () => ({
|
|
313
|
+
"cloud.provider": "gcp",
|
|
314
|
+
"cloud.platform": "gcp_cloud_run",
|
|
315
|
+
...env.K_SERVICE && { "faas.name": env.K_SERVICE },
|
|
316
|
+
...env.K_REVISION && { "faas.version": env.K_REVISION },
|
|
317
|
+
...env.CLOUD_RUN_REGION && { "cloud.region": env.CLOUD_RUN_REGION }
|
|
318
|
+
})), Match.when("firebase_app_hosting", () => ({
|
|
319
|
+
"cloud.provider": "gcp",
|
|
320
|
+
"cloud.platform": "gcp_firebase_app_hosting"
|
|
321
|
+
})), Match.when("aws_amplify", () => ({
|
|
322
|
+
"cloud.provider": "aws",
|
|
323
|
+
"cloud.platform": "aws_amplify"
|
|
324
|
+
})), Match.when("deno-deploy", () => ({
|
|
325
|
+
"cloud.provider": "deno",
|
|
326
|
+
"cloud.platform": "deno_deploy",
|
|
327
|
+
...env.DENO_REGION && { "cloud.region": env.DENO_REGION },
|
|
328
|
+
...env.DENO_DEPLOYMENT_ID && { "faas.instance": env.DENO_DEPLOYMENT_ID }
|
|
329
|
+
})), Match.when("render", () => ({
|
|
330
|
+
"cloud.provider": "render",
|
|
331
|
+
"cloud.platform": "render",
|
|
332
|
+
...env.RENDER_INSTANCE_ID && { "faas.instance": env.RENDER_INSTANCE_ID }
|
|
333
|
+
})), Match.when("railway", () => ({
|
|
334
|
+
"cloud.provider": "railway",
|
|
335
|
+
"cloud.platform": "railway",
|
|
336
|
+
...env.RAILWAY_REPLICA_ID && { "faas.instance": env.RAILWAY_REPLICA_ID }
|
|
337
|
+
})), Match.when("edgeone_pages", () => ({
|
|
338
|
+
"cloud.provider": "tencent_cloud",
|
|
339
|
+
"cloud.platform": "tencent_edgeone_pages"
|
|
340
|
+
})), Match.orElse(() => empty));
|
|
341
|
+
const workerdFallback = (rt, alreadyResolved) => rt === "workerd" && !alreadyResolved ? {
|
|
342
|
+
"cloud.provider": "cloudflare",
|
|
343
|
+
"cloud.platform": "cloudflare.workers"
|
|
344
|
+
} : empty;
|
|
345
|
+
const vercelEdgeFallback = (rt, alreadyResolved, env) => rt === "edge-light" && !alreadyResolved && env.VERCEL ? {
|
|
346
|
+
"cloud.provider": "vercel",
|
|
347
|
+
"cloud.platform": "vercel"
|
|
348
|
+
} : empty;
|
|
349
|
+
const derivePlatformAttributes = (inputs) => {
|
|
350
|
+
const { runtime: rt, provider: prov, platform: plat, arch, env } = inputs;
|
|
351
|
+
const cloudAttrs = env.AWS_LAMBDA_FUNCTION_NAME ? lambdaAttrs(env) : providerAttrs(prov, env);
|
|
352
|
+
const cloudResolved = "cloud.provider" in cloudAttrs;
|
|
353
|
+
return {
|
|
354
|
+
...rt && { "mxl.runtime": rt },
|
|
355
|
+
...prov && { "mxl.provider": prov },
|
|
356
|
+
...runtimeAttrs(rt),
|
|
357
|
+
...osAttrs(plat),
|
|
358
|
+
...archAttrs(arch),
|
|
359
|
+
...cloudAttrs,
|
|
360
|
+
...workerdFallback(rt, cloudResolved),
|
|
361
|
+
...vercelEdgeFallback(rt, cloudResolved, env)
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
const PLATFORM_ENV_KEYS = [
|
|
365
|
+
"AWS_LAMBDA_FUNCTION_NAME",
|
|
366
|
+
"AWS_LAMBDA_FUNCTION_VERSION",
|
|
367
|
+
"AWS_LAMBDA_LOG_STREAM_NAME",
|
|
368
|
+
"AWS_REGION",
|
|
369
|
+
"AWS_DEFAULT_REGION",
|
|
370
|
+
"VERCEL",
|
|
371
|
+
"VERCEL_REGION",
|
|
372
|
+
"VERCEL_DEPLOYMENT_ID",
|
|
373
|
+
"K_SERVICE",
|
|
374
|
+
"K_REVISION",
|
|
375
|
+
"CLOUD_RUN_REGION",
|
|
376
|
+
"DENO_REGION",
|
|
377
|
+
"DENO_DEPLOYMENT_ID",
|
|
378
|
+
"RENDER_INSTANCE_ID",
|
|
379
|
+
"RAILWAY_REPLICA_ID"
|
|
380
|
+
];
|
|
381
|
+
const getAutoPlatformAttributes = () => {
|
|
382
|
+
const proc = globalThis.process;
|
|
383
|
+
const procEnv = proc?.env ?? {};
|
|
384
|
+
const env = {};
|
|
385
|
+
for (const key of PLATFORM_ENV_KEYS) env[key] = procEnv[key];
|
|
386
|
+
return derivePlatformAttributes({
|
|
387
|
+
runtime: runtime ?? "",
|
|
388
|
+
provider: provider ?? "",
|
|
389
|
+
platform: platform ?? "",
|
|
390
|
+
arch: proc?.arch ?? "",
|
|
391
|
+
env
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/server/resource.ts
|
|
396
|
+
const stringOrUndefined = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
397
|
+
const DEFAULT_VCS_REPOSITORY_URL = "https://gitlab.safaricom.co.ke/mxl/apm/mxl-effect-sdk";
|
|
398
|
+
function resolveCommitRevision() {
|
|
399
|
+
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);
|
|
400
|
+
}
|
|
401
|
+
let serviceInstanceId;
|
|
402
|
+
const getServiceInstanceId = () => serviceInstanceId ??= v7();
|
|
403
|
+
const resolveResource = (config) => Effect.gen(function* () {
|
|
404
|
+
if (yield* sdkDisabled) return {
|
|
405
|
+
endpoint: void 0,
|
|
406
|
+
environment: void 0,
|
|
407
|
+
headers: void 0,
|
|
408
|
+
resource: {
|
|
409
|
+
attributes: {},
|
|
410
|
+
serviceName: config.serviceName ?? "unknown",
|
|
411
|
+
serviceVersion: void 0
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const envEndpoint = yield* endpoint;
|
|
415
|
+
const endpoint$1 = config.endpoint ?? Option.getOrUndefined(envEndpoint) ?? "http://127.0.0.1:4318";
|
|
416
|
+
const envHeaders = yield* otlpHeaders;
|
|
417
|
+
const headers = config.headers && Object.keys(config.headers).length > 0 ? config.headers : Object.keys(envHeaders).length > 0 ? envHeaders : void 0;
|
|
418
|
+
const envServiceVersion = yield* serviceVersion;
|
|
419
|
+
const serviceVersion$1 = config.serviceVersion ?? Option.getOrUndefined(envServiceVersion);
|
|
420
|
+
const envEnvironment = yield* environment;
|
|
421
|
+
const environment$1 = config.environment ?? envEnvironment;
|
|
422
|
+
const envOtelServiceName = yield* otelServiceName;
|
|
423
|
+
const serviceName = config.serviceName ?? Option.getOrUndefined(envOtelServiceName) ?? "unknown";
|
|
424
|
+
const envResourceAttributes = yield* otelResourceAttributes;
|
|
425
|
+
const attributes = {};
|
|
426
|
+
Object.assign(attributes, getAutoPlatformAttributes());
|
|
427
|
+
attributes["mxl.sdk.type"] = config.sdkType ?? "server";
|
|
428
|
+
attributes["service.instance.id"] = getServiceInstanceId();
|
|
429
|
+
if (environment$1) {
|
|
430
|
+
attributes["deployment.environment"] = environment$1;
|
|
431
|
+
attributes["deployment.environment.name"] = environment$1;
|
|
432
|
+
}
|
|
433
|
+
if (serviceVersion$1) attributes["service.version"] = serviceVersion$1;
|
|
434
|
+
const commitRevision = resolveCommitRevision();
|
|
435
|
+
if (commitRevision) {
|
|
436
|
+
attributes["deployment.commit_sha"] = commitRevision;
|
|
437
|
+
attributes["vcs.ref.head.revision"] = commitRevision;
|
|
438
|
+
}
|
|
439
|
+
attributes["vcs.repository.url.full"] = stringOrUndefined(process.env.MXL_VCS_REPOSITORY_URL) ?? DEFAULT_VCS_REPOSITORY_URL;
|
|
440
|
+
Object.assign(attributes, envResourceAttributes);
|
|
441
|
+
if (config.attributes) Object.assign(attributes, config.attributes);
|
|
442
|
+
return {
|
|
443
|
+
endpoint: endpoint$1,
|
|
444
|
+
environment: environment$1,
|
|
445
|
+
headers,
|
|
446
|
+
resource: {
|
|
447
|
+
serviceName,
|
|
448
|
+
serviceVersion: serviceVersion$1,
|
|
449
|
+
attributes
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}).pipe(Effect.orDie);
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/server/layer.ts
|
|
455
|
+
/**
|
|
456
|
+
* Create an Effect Layer that provides OpenTelemetry traces, logs, and metrics
|
|
457
|
+
* configured for MXL's OTLP collector.
|
|
458
|
+
*
|
|
459
|
+
* Returns a no-op layer when `OTEL_SDK_DISABLED=true`.
|
|
460
|
+
*/
|
|
461
|
+
const layer = (config = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
462
|
+
const resolved = yield* resolveResource({
|
|
463
|
+
...config,
|
|
464
|
+
sdkType: "server"
|
|
465
|
+
});
|
|
466
|
+
if (!resolved.endpoint) return Layer.empty;
|
|
467
|
+
return Otlp.layerJson({
|
|
468
|
+
baseUrl: normalizeOtlpBaseUrl(resolved.endpoint),
|
|
469
|
+
headers: resolved.headers,
|
|
470
|
+
resource: resolved.resource,
|
|
471
|
+
maxBatchSize: config.maxBatchSize,
|
|
472
|
+
loggerExportInterval: config.loggerExportInterval,
|
|
473
|
+
metricsExportInterval: config.metricsExportInterval,
|
|
474
|
+
tracerExportInterval: config.tracerExportInterval,
|
|
475
|
+
shutdownTimeout: config.shutdownTimeout
|
|
476
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
477
|
+
})).pipe(Layer.updateService(Tracer.Tracer, wrapTracerWithOtelContextBridge));
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/shared/flush-core.ts
|
|
480
|
+
const COOLDOWN_MS = 6e4;
|
|
481
|
+
const makeSerializedFlush = (flush) => {
|
|
482
|
+
let pending = Promise.resolve();
|
|
483
|
+
return (...args) => {
|
|
484
|
+
pending = pending.then(() => flush(...args), () => flush(...args));
|
|
485
|
+
return pending;
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
const guardFlush = (prefix, flush, state) => {
|
|
489
|
+
return async (...args) => {
|
|
490
|
+
if (state && Date.now() < state.disabledUntil) return;
|
|
491
|
+
try {
|
|
492
|
+
await flush(...args);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (state) state.disabledUntil = Date.now() + COOLDOWN_MS;
|
|
495
|
+
console.error(`${prefix} flush failed; cooling down 60s`, error);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/cloudflare/index.ts
|
|
501
|
+
var WorkerEnvironment = class extends Context.Service()("Cloudflare.Workers.WorkerEnvironment") {};
|
|
502
|
+
const stringFromEnv = (env, key) => {
|
|
503
|
+
const value = env[key];
|
|
504
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
505
|
+
};
|
|
506
|
+
const resolveEventsConfig = (env, config) => ({
|
|
507
|
+
apiKey: config.apiKey ?? stringFromEnv(env, "MXL_API_KEY"),
|
|
508
|
+
clientId: config.clientId ?? stringFromEnv(env, "MXL_CLIENT_ID"),
|
|
509
|
+
endpoint: config.ingestUrl ?? stringFromEnv(env, "MXL_BASKET_URL") ?? void 0,
|
|
510
|
+
flushInterval: 0,
|
|
511
|
+
serviceName: config.serviceName ?? stringFromEnv(env, "OTEL_SERVICE_NAME") ?? "unknown"
|
|
512
|
+
});
|
|
513
|
+
const make = (config = {}) => {
|
|
514
|
+
let handle;
|
|
515
|
+
const flush = makeSerializedFlush(guardFlush("[MxlCloudflareSDK]", async (env) => {
|
|
516
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
517
|
+
const eventsConfig = resolveEventsConfig(env, config);
|
|
518
|
+
if (!eventsConfig.clientId) return;
|
|
519
|
+
handle ??= makeHandle(eventsConfig);
|
|
520
|
+
await handle.flush();
|
|
521
|
+
}, { disabledUntil: 0 }));
|
|
522
|
+
const layer$2 = layer({
|
|
523
|
+
attributes: Object.fromEntries(Object.entries(config.attributes ?? {}).map(([key, value]) => [key, String(value)])),
|
|
524
|
+
endpoint: config.endpoint,
|
|
525
|
+
environment: config.environment,
|
|
526
|
+
serviceName: config.serviceName,
|
|
527
|
+
serviceVersion: config.serviceVersion
|
|
528
|
+
});
|
|
529
|
+
return {
|
|
530
|
+
flush,
|
|
531
|
+
layer: layer$2,
|
|
532
|
+
requestLayer: Layer.mergeAll(layer$2, Layer.unwrap(Effect.gen(function* () {
|
|
533
|
+
const env = yield* WorkerEnvironment;
|
|
534
|
+
const eventsConfig = resolveEventsConfig(env, config);
|
|
535
|
+
if (!eventsConfig.clientId) return Layer.empty;
|
|
536
|
+
return layer$1(eventsConfig);
|
|
537
|
+
})), Layer.effectDiscard(Effect.gen(function* () {
|
|
538
|
+
const env = yield* WorkerEnvironment;
|
|
539
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => flush(env)));
|
|
540
|
+
})))
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
const MxlCloudflareSDK = { make };
|
|
544
|
+
//#endregion
|
|
545
|
+
export { MxlCloudflareSDK, WorkerEnvironment, make };
|
|
546
|
+
|
|
547
|
+
//# sourceMappingURL=index.js.map
|