@sylphx/sdk 0.12.0 → 0.12.2
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/dist/health/index.d.ts +734 -58
- package/dist/health/index.mjs +867 -4
- package/dist/health/index.mjs.map +1 -1
- package/dist/index.d.ts +11 -12
- package/dist/index.mjs +32 -25
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.mjs +7 -1
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.mjs +9 -2
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.mjs +7 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/web-analytics.mjs +7 -1
- package/dist/web-analytics.mjs.map +1 -1
- package/package.json +9 -4
package/dist/health/index.mjs
CHANGED
|
@@ -1,3 +1,181 @@
|
|
|
1
|
+
// src/health/causality.ts
|
|
2
|
+
var BAGGAGE_KEY_CAUSE = "sylphx.health.cause";
|
|
3
|
+
var BAGGAGE_KEY_CHAIN = "sylphx.health.cause_chain";
|
|
4
|
+
var DEFAULT_CAUSE_THRESHOLD = 0.5;
|
|
5
|
+
var MAX_CHAIN_LENGTH = 5;
|
|
6
|
+
var cachedApi;
|
|
7
|
+
async function tryLoadCausalityApi() {
|
|
8
|
+
if (cachedApi !== void 0) return cachedApi;
|
|
9
|
+
try {
|
|
10
|
+
const api = await import("@opentelemetry/api");
|
|
11
|
+
if (!api?.context || !api?.propagation) {
|
|
12
|
+
cachedApi = null;
|
|
13
|
+
return cachedApi;
|
|
14
|
+
}
|
|
15
|
+
cachedApi = { context: api.context, propagation: api.propagation };
|
|
16
|
+
} catch {
|
|
17
|
+
cachedApi = null;
|
|
18
|
+
}
|
|
19
|
+
return cachedApi;
|
|
20
|
+
}
|
|
21
|
+
function defaultServiceName() {
|
|
22
|
+
if (typeof process !== "undefined" && process.env) {
|
|
23
|
+
if (process.env.OTEL_SERVICE_NAME !== void 0) return process.env.OTEL_SERVICE_NAME;
|
|
24
|
+
if (process.env.SERVICE_NAME !== void 0) return process.env.SERVICE_NAME;
|
|
25
|
+
}
|
|
26
|
+
return "unknown";
|
|
27
|
+
}
|
|
28
|
+
function isSafeIdentifier(part) {
|
|
29
|
+
return part.length > 0 && !/[,@\s]/.test(part);
|
|
30
|
+
}
|
|
31
|
+
function encodeCauseFromScore(score, threshold, serviceName) {
|
|
32
|
+
const factors = score.signalFactors;
|
|
33
|
+
if (!factors) return null;
|
|
34
|
+
const entries = [];
|
|
35
|
+
for (const [signalName, factor] of Object.entries(factors)) {
|
|
36
|
+
if (typeof factor !== "number" || !Number.isFinite(factor)) continue;
|
|
37
|
+
if (factor >= threshold) continue;
|
|
38
|
+
if (!isSafeIdentifier(signalName) || !isSafeIdentifier(serviceName)) continue;
|
|
39
|
+
entries.push(`${signalName}@${serviceName}`);
|
|
40
|
+
}
|
|
41
|
+
if (entries.length === 0) return null;
|
|
42
|
+
return entries.join(",");
|
|
43
|
+
}
|
|
44
|
+
function parseCauseValue(value) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const raw of value.split(",")) {
|
|
47
|
+
const piece = raw.trim();
|
|
48
|
+
if (piece.length === 0) continue;
|
|
49
|
+
const atIdx = piece.indexOf("@");
|
|
50
|
+
if (atIdx <= 0 || atIdx === piece.length - 1) continue;
|
|
51
|
+
const signal = piece.slice(0, atIdx);
|
|
52
|
+
const service = piece.slice(atIdx + 1);
|
|
53
|
+
if (!isSafeIdentifier(signal) || !isSafeIdentifier(service)) continue;
|
|
54
|
+
out.push({ signal, service });
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function parseChainValue(value) {
|
|
59
|
+
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && isSafeIdentifier(s));
|
|
60
|
+
}
|
|
61
|
+
function appendChain(prev, serviceName, maxLen) {
|
|
62
|
+
if (!isSafeIdentifier(serviceName)) return prev;
|
|
63
|
+
if (prev[prev.length - 1] === serviceName) return prev;
|
|
64
|
+
const next = [...prev, serviceName];
|
|
65
|
+
if (next.length <= maxLen) return next;
|
|
66
|
+
return next.slice(next.length - maxLen);
|
|
67
|
+
}
|
|
68
|
+
var CausalityHandleImpl = class {
|
|
69
|
+
_state = "loading";
|
|
70
|
+
apiRef = null;
|
|
71
|
+
threshold;
|
|
72
|
+
serviceName;
|
|
73
|
+
propagateChain;
|
|
74
|
+
constructor(opts) {
|
|
75
|
+
const threshold = opts.threshold ?? DEFAULT_CAUSE_THRESHOLD;
|
|
76
|
+
if (!Number.isFinite(threshold) || threshold <= 0 || threshold > 1) {
|
|
77
|
+
throw new Error(`createCausalityHandle: threshold must be in (0, 1], got ${threshold}`);
|
|
78
|
+
}
|
|
79
|
+
this.threshold = threshold;
|
|
80
|
+
this.serviceName = opts.serviceName ?? defaultServiceName();
|
|
81
|
+
this.propagateChain = opts.propagateChain !== false;
|
|
82
|
+
void this.initialize();
|
|
83
|
+
}
|
|
84
|
+
get state() {
|
|
85
|
+
return this._state;
|
|
86
|
+
}
|
|
87
|
+
async initialize() {
|
|
88
|
+
const api = await tryLoadCausalityApi();
|
|
89
|
+
if (this._state === "disposed") return;
|
|
90
|
+
if (api === null) {
|
|
91
|
+
this._state = "unavailable";
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.apiRef = api;
|
|
95
|
+
this._state = "ready";
|
|
96
|
+
}
|
|
97
|
+
readCause() {
|
|
98
|
+
if (this._state !== "ready" || !this.apiRef) return null;
|
|
99
|
+
try {
|
|
100
|
+
const baggage = this.apiRef.propagation.getActiveBaggage();
|
|
101
|
+
if (!baggage) return null;
|
|
102
|
+
const causeEntry = baggage.getEntry(BAGGAGE_KEY_CAUSE);
|
|
103
|
+
if (!causeEntry || typeof causeEntry.value !== "string") return null;
|
|
104
|
+
const causes = parseCauseValue(causeEntry.value);
|
|
105
|
+
if (causes.length === 0) return null;
|
|
106
|
+
const chainEntry = baggage.getEntry(BAGGAGE_KEY_CHAIN);
|
|
107
|
+
const chain = chainEntry && typeof chainEntry.value === "string" ? parseChainValue(chainEntry.value) : [];
|
|
108
|
+
return { causes, chain };
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
encodeCause(score) {
|
|
114
|
+
try {
|
|
115
|
+
return encodeCauseFromScore(score, this.threshold, this.serviceName);
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async runWithCause(fn, score) {
|
|
121
|
+
if (this._state !== "ready" || !this.apiRef) {
|
|
122
|
+
return await fn();
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const localCause = this.encodeCause(score);
|
|
126
|
+
const existing = this.apiRef.propagation.getActiveBaggage();
|
|
127
|
+
const existingChain = existing?.getEntry(BAGGAGE_KEY_CHAIN)?.value ? parseChainValue(existing.getEntry(BAGGAGE_KEY_CHAIN).value) : [];
|
|
128
|
+
if (!localCause && !existing) {
|
|
129
|
+
return await fn();
|
|
130
|
+
}
|
|
131
|
+
const baseBaggage = existing ?? this.apiRef.propagation.createBaggage();
|
|
132
|
+
let nextBaggage = baseBaggage;
|
|
133
|
+
if (localCause) {
|
|
134
|
+
const newEntry = { value: localCause };
|
|
135
|
+
const upstream = existing?.getEntry(BAGGAGE_KEY_CAUSE)?.value;
|
|
136
|
+
const mergedValue = upstream ? mergeCauseValues(upstream, localCause) : localCause;
|
|
137
|
+
nextBaggage = nextBaggage.setEntry(BAGGAGE_KEY_CAUSE, {
|
|
138
|
+
...newEntry,
|
|
139
|
+
value: mergedValue
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (this.propagateChain && (localCause || existingChain.length > 0)) {
|
|
143
|
+
const nextChain = localCause ? appendChain(existingChain, this.serviceName, MAX_CHAIN_LENGTH) : existingChain;
|
|
144
|
+
if (nextChain.length > 0) {
|
|
145
|
+
nextBaggage = nextBaggage.setEntry(BAGGAGE_KEY_CHAIN, {
|
|
146
|
+
value: nextChain.join(",")
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const ctxWithBaggage = this.apiRef.propagation.setBaggage(
|
|
151
|
+
this.apiRef.context.active(),
|
|
152
|
+
nextBaggage
|
|
153
|
+
);
|
|
154
|
+
return await this.apiRef.context.with(ctxWithBaggage, fn);
|
|
155
|
+
} catch {
|
|
156
|
+
return await fn();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
dispose() {
|
|
160
|
+
this._state = "disposed";
|
|
161
|
+
this.apiRef = null;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
function mergeCauseValues(existing, local) {
|
|
165
|
+
const seen = /* @__PURE__ */ new Set();
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const piece of [...existing.split(","), ...local.split(",")]) {
|
|
168
|
+
const trimmed = piece.trim();
|
|
169
|
+
if (trimmed.length === 0 || seen.has(trimmed)) continue;
|
|
170
|
+
seen.add(trimmed);
|
|
171
|
+
out.push(trimmed);
|
|
172
|
+
}
|
|
173
|
+
return out.join(",");
|
|
174
|
+
}
|
|
175
|
+
function createCausalityHandle(opts = {}) {
|
|
176
|
+
return new CausalityHandleImpl(opts);
|
|
177
|
+
}
|
|
178
|
+
|
|
1
179
|
// src/health/effects.ts
|
|
2
180
|
import { Effect } from "effect";
|
|
3
181
|
|
|
@@ -21,12 +199,16 @@ function evaluateEffect(evaluator) {
|
|
|
21
199
|
}
|
|
22
200
|
|
|
23
201
|
// src/health/handler.ts
|
|
202
|
+
var DIRECT_PROBE_LIVENESS_FLOOR = 0.5;
|
|
203
|
+
function statusForScore(score) {
|
|
204
|
+
return Number.isFinite(score) && score >= DIRECT_PROBE_LIVENESS_FLOOR ? 200 : 503;
|
|
205
|
+
}
|
|
24
206
|
function createWebHandler(source) {
|
|
25
207
|
return async (_req) => {
|
|
26
208
|
try {
|
|
27
209
|
const score = await source.evaluate();
|
|
28
210
|
return new Response(JSON.stringify(score), {
|
|
29
|
-
status:
|
|
211
|
+
status: statusForScore(score.score),
|
|
30
212
|
headers: {
|
|
31
213
|
"Content-Type": "application/json",
|
|
32
214
|
// Cache-Control: never cache. Even 1-second cache on a
|
|
@@ -56,7 +238,7 @@ function createNodeHandler(source) {
|
|
|
56
238
|
return async (_req, res) => {
|
|
57
239
|
try {
|
|
58
240
|
const score = await source.evaluate();
|
|
59
|
-
res.statusCode =
|
|
241
|
+
res.statusCode = statusForScore(score.score);
|
|
60
242
|
res.setHeader("Content-Type", "application/json");
|
|
61
243
|
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
62
244
|
res.end(JSON.stringify(score));
|
|
@@ -70,6 +252,387 @@ function createNodeHandler(source) {
|
|
|
70
252
|
};
|
|
71
253
|
}
|
|
72
254
|
|
|
255
|
+
// src/health/history.ts
|
|
256
|
+
var GENESIS_PREV_HASH = "0".repeat(64);
|
|
257
|
+
var DEFAULT_HISTORY_CAPACITY = 1e3;
|
|
258
|
+
var HASH_TEXT_ENCODER = new TextEncoder();
|
|
259
|
+
var HEX_BYTE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
260
|
+
function canonicalizeSignals(signals) {
|
|
261
|
+
const keys = Object.keys(signals).sort();
|
|
262
|
+
const pairs = keys.map((k) => {
|
|
263
|
+
const value = signals[k];
|
|
264
|
+
return [k, value];
|
|
265
|
+
});
|
|
266
|
+
return JSON.stringify(Object.fromEntries(pairs));
|
|
267
|
+
}
|
|
268
|
+
function encodeScore(score) {
|
|
269
|
+
return score.toFixed(6);
|
|
270
|
+
}
|
|
271
|
+
async function sha256Hex(input) {
|
|
272
|
+
const subtle = globalThis.crypto?.subtle;
|
|
273
|
+
if (!subtle || typeof subtle.digest !== "function") {
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
const buf = HASH_TEXT_ENCODER.encode(input);
|
|
277
|
+
const digest = await subtle.digest("SHA-256", buf);
|
|
278
|
+
const bytes = new Uint8Array(digest);
|
|
279
|
+
let out = "";
|
|
280
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
281
|
+
out += HEX_BYTE[bytes[i]];
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
async function computeEntryHash(prehash) {
|
|
286
|
+
const canonical = [
|
|
287
|
+
prehash.sequence.toString(10),
|
|
288
|
+
prehash.evaluatedAt,
|
|
289
|
+
encodeScore(prehash.score),
|
|
290
|
+
prehash.signalsDigest,
|
|
291
|
+
prehash.prevHash
|
|
292
|
+
].join("|");
|
|
293
|
+
return await sha256Hex(canonical);
|
|
294
|
+
}
|
|
295
|
+
var InMemoryHistoryStoreImpl = class {
|
|
296
|
+
capacity;
|
|
297
|
+
buf = [];
|
|
298
|
+
constructor(opts = {}) {
|
|
299
|
+
const cap = opts.capacity ?? DEFAULT_HISTORY_CAPACITY;
|
|
300
|
+
if (!Number.isFinite(cap) || cap <= 0 || cap > 1e7) {
|
|
301
|
+
throw new Error(`InMemoryHistoryStore: capacity must be in (0, 10000000], got ${cap}`);
|
|
302
|
+
}
|
|
303
|
+
this.capacity = cap;
|
|
304
|
+
}
|
|
305
|
+
append(entry) {
|
|
306
|
+
this.buf.push(entry);
|
|
307
|
+
if (this.buf.length > this.capacity) {
|
|
308
|
+
this.buf.shift();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
range(query) {
|
|
312
|
+
const out = [];
|
|
313
|
+
for (const e of this.buf) {
|
|
314
|
+
if (query.fromSequence !== void 0 && e.sequence < query.fromSequence) continue;
|
|
315
|
+
if (query.toSequence !== void 0 && e.sequence > query.toSequence) continue;
|
|
316
|
+
if (query.fromTimestamp !== void 0 && e.evaluatedAt < query.fromTimestamp) continue;
|
|
317
|
+
if (query.toTimestamp !== void 0 && e.evaluatedAt > query.toTimestamp) continue;
|
|
318
|
+
out.push(e);
|
|
319
|
+
if (query.limit !== void 0 && out.length >= query.limit) break;
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
latest() {
|
|
324
|
+
return this.buf[this.buf.length - 1] ?? null;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
function createInMemoryHistoryStore(opts = {}) {
|
|
328
|
+
return new InMemoryHistoryStoreImpl(opts);
|
|
329
|
+
}
|
|
330
|
+
var HistoryRecorderImpl = class {
|
|
331
|
+
store;
|
|
332
|
+
nextSequence = 0;
|
|
333
|
+
prevHash = GENESIS_PREV_HASH;
|
|
334
|
+
disposed = false;
|
|
335
|
+
initPromise;
|
|
336
|
+
/**
|
|
337
|
+
* Serializes record() calls so concurrent `evaluate()` invocations
|
|
338
|
+
* don't race on `nextSequence` / `prevHash`. Each new call chains off
|
|
339
|
+
* the previous; the chain itself never propagates errors (catch in the
|
|
340
|
+
* tail so one failed record doesn't poison subsequent ones).
|
|
341
|
+
*/
|
|
342
|
+
inFlight = Promise.resolve();
|
|
343
|
+
constructor(opts) {
|
|
344
|
+
this.store = opts.store ?? createInMemoryHistoryStore({ capacity: opts.capacity });
|
|
345
|
+
this.initPromise = this.warmStart();
|
|
346
|
+
}
|
|
347
|
+
async warmStart() {
|
|
348
|
+
try {
|
|
349
|
+
const latest = await this.store.latest();
|
|
350
|
+
if (latest && Number.isFinite(latest.sequence)) {
|
|
351
|
+
this.nextSequence = latest.sequence + 1;
|
|
352
|
+
this.prevHash = latest.entryHash || GENESIS_PREV_HASH;
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async recordInternal(score) {
|
|
358
|
+
if (this.disposed) return null;
|
|
359
|
+
await this.initPromise;
|
|
360
|
+
try {
|
|
361
|
+
const signalsDigest = await sha256Hex(canonicalizeSignals(score.signals));
|
|
362
|
+
const prehash = {
|
|
363
|
+
sequence: this.nextSequence,
|
|
364
|
+
evaluatedAt: score.lastTickAt,
|
|
365
|
+
score: score.score,
|
|
366
|
+
signalsDigest,
|
|
367
|
+
prevHash: this.prevHash
|
|
368
|
+
};
|
|
369
|
+
const entryHash = await computeEntryHash(prehash);
|
|
370
|
+
const entry = { ...prehash, entryHash };
|
|
371
|
+
await this.store.append(entry);
|
|
372
|
+
this.nextSequence += 1;
|
|
373
|
+
this.prevHash = entryHash || GENESIS_PREV_HASH;
|
|
374
|
+
return entry;
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async record(score) {
|
|
380
|
+
const job = this.inFlight.then(() => this.recordInternal(score));
|
|
381
|
+
this.inFlight = job.catch(() => void 0);
|
|
382
|
+
return await job;
|
|
383
|
+
}
|
|
384
|
+
async drainPendingRecords() {
|
|
385
|
+
try {
|
|
386
|
+
await this.inFlight;
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async range(query) {
|
|
391
|
+
await this.drainPendingRecords();
|
|
392
|
+
try {
|
|
393
|
+
return await this.store.range(query);
|
|
394
|
+
} catch {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async latest() {
|
|
399
|
+
await this.drainPendingRecords();
|
|
400
|
+
try {
|
|
401
|
+
return await this.store.latest();
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async verify(query) {
|
|
407
|
+
const entries = await this.range(query);
|
|
408
|
+
return await verifyChain(entries);
|
|
409
|
+
}
|
|
410
|
+
dispose() {
|
|
411
|
+
this.disposed = true;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
function createHistoryRecorder(opts = {}) {
|
|
415
|
+
return new HistoryRecorderImpl(opts);
|
|
416
|
+
}
|
|
417
|
+
async function verifyChain(entries) {
|
|
418
|
+
if (entries.length === 0) {
|
|
419
|
+
return { verified: true, entriesChecked: 0, firstBreakAtSequence: null, reason: null };
|
|
420
|
+
}
|
|
421
|
+
let expectedPrev = null;
|
|
422
|
+
let checked = 0;
|
|
423
|
+
for (let i = 0; i < entries.length; i++) {
|
|
424
|
+
const entry = entries[i];
|
|
425
|
+
if (i === 0 && entry.sequence === 0 && entry.prevHash !== GENESIS_PREV_HASH) {
|
|
426
|
+
return {
|
|
427
|
+
verified: false,
|
|
428
|
+
entriesChecked: checked,
|
|
429
|
+
firstBreakAtSequence: entry.sequence,
|
|
430
|
+
reason: `genesis entry must have prevHash="${GENESIS_PREV_HASH}", got ${entry.prevHash}`
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (i > 0 && expectedPrev !== null && entry.prevHash !== expectedPrev) {
|
|
434
|
+
return {
|
|
435
|
+
verified: false,
|
|
436
|
+
entriesChecked: checked,
|
|
437
|
+
firstBreakAtSequence: entry.sequence,
|
|
438
|
+
reason: `prevHash mismatch at sequence ${entry.sequence}: expected ${expectedPrev}, got ${entry.prevHash}`
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const recomputedHash = await computeEntryHash({
|
|
442
|
+
sequence: entry.sequence,
|
|
443
|
+
evaluatedAt: entry.evaluatedAt,
|
|
444
|
+
score: entry.score,
|
|
445
|
+
signalsDigest: entry.signalsDigest,
|
|
446
|
+
prevHash: entry.prevHash
|
|
447
|
+
});
|
|
448
|
+
if (entry.entryHash && recomputedHash && recomputedHash !== entry.entryHash) {
|
|
449
|
+
return {
|
|
450
|
+
verified: false,
|
|
451
|
+
entriesChecked: checked,
|
|
452
|
+
firstBreakAtSequence: entry.sequence,
|
|
453
|
+
reason: `entryHash mismatch at sequence ${entry.sequence}: stored=${entry.entryHash} recomputed=${recomputedHash}`
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
expectedPrev = entry.entryHash || null;
|
|
457
|
+
checked += 1;
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
verified: true,
|
|
461
|
+
entriesChecked: checked,
|
|
462
|
+
firstBreakAtSequence: null,
|
|
463
|
+
reason: null
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/health/otel.ts
|
|
468
|
+
var SDK_NAME = "@sylphx/sdk/health";
|
|
469
|
+
var SDK_VERSION = "0.11.0";
|
|
470
|
+
var METRIC_NAMES = {
|
|
471
|
+
score: "health.score",
|
|
472
|
+
signalFactor: "health.signal.factor",
|
|
473
|
+
signalValue: "health.signal.value",
|
|
474
|
+
signalValueState: "health.signal.value_state",
|
|
475
|
+
/** ADR-143 §3.3 — increments per evaluation when an upstream cause is observed. */
|
|
476
|
+
downstreamCause: "health.downstream_cause"
|
|
477
|
+
};
|
|
478
|
+
var SPAN_ATTRIBUTES = {
|
|
479
|
+
score: "service.health.score",
|
|
480
|
+
signalFactor: (name) => `service.health.signal.${name}.factor`,
|
|
481
|
+
/** ADR-143 §3.2 — the upstream service that originated the failure chain. */
|
|
482
|
+
causeRoot: "service.health.cause_root",
|
|
483
|
+
/** ADR-143 §3.2 — the signal at the root that failed. */
|
|
484
|
+
causeSignal: "service.health.cause_signal",
|
|
485
|
+
/** ADR-143 §3.2 — comma-delimited hop chain (oldest to newest). */
|
|
486
|
+
causeChain: "service.health.cause_chain"
|
|
487
|
+
};
|
|
488
|
+
var EVENT_NAMES = {
|
|
489
|
+
evaluated: "health.evaluated"
|
|
490
|
+
};
|
|
491
|
+
function createOtelEmitter(opts = {}) {
|
|
492
|
+
return new OtelEmitterImpl(opts);
|
|
493
|
+
}
|
|
494
|
+
var OtelEmitterImpl = class {
|
|
495
|
+
constructor(opts) {
|
|
496
|
+
this.opts = opts;
|
|
497
|
+
this.staticAttrs = opts.attributes ?? {};
|
|
498
|
+
this.events = opts.events === true;
|
|
499
|
+
if (opts.meter) {
|
|
500
|
+
this.instruments = buildInstruments(opts.meter);
|
|
501
|
+
this._state = "ready";
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
void this.initialize();
|
|
505
|
+
}
|
|
506
|
+
_state = "loading";
|
|
507
|
+
instruments = null;
|
|
508
|
+
traceApi = null;
|
|
509
|
+
staticAttrs;
|
|
510
|
+
events;
|
|
511
|
+
get state() {
|
|
512
|
+
return this._state;
|
|
513
|
+
}
|
|
514
|
+
async initialize() {
|
|
515
|
+
const api = await tryLoadOtelApi();
|
|
516
|
+
if (api === null) {
|
|
517
|
+
this._state = "unavailable";
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (this._state === "disposed") return;
|
|
521
|
+
try {
|
|
522
|
+
const meter = api.metrics.getMeter(SDK_NAME, SDK_VERSION);
|
|
523
|
+
this.instruments = buildInstruments(meter);
|
|
524
|
+
this.traceApi = api.trace;
|
|
525
|
+
this._state = "ready";
|
|
526
|
+
} catch {
|
|
527
|
+
this._state = "unavailable";
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
emit(score, upstreamCause) {
|
|
531
|
+
if (this._state !== "ready" || !this.instruments) return;
|
|
532
|
+
const baseAttrs = { ...this.staticAttrs };
|
|
533
|
+
try {
|
|
534
|
+
this.instruments.scoreGauge.record(score.score, baseAttrs);
|
|
535
|
+
for (const [signalName, rawValue] of Object.entries(score.signals)) {
|
|
536
|
+
const signalAttrs = { ...baseAttrs, "signal.name": signalName };
|
|
537
|
+
const factor = score.signalFactors?.[signalName];
|
|
538
|
+
if (typeof factor === "number" && Number.isFinite(factor)) {
|
|
539
|
+
this.instruments.factorGauge.record(factor, signalAttrs);
|
|
540
|
+
}
|
|
541
|
+
if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
|
|
542
|
+
this.instruments.valueGauge.record(rawValue, signalAttrs);
|
|
543
|
+
} else {
|
|
544
|
+
const stateAttrs = {
|
|
545
|
+
...signalAttrs,
|
|
546
|
+
state: String(rawValue)
|
|
547
|
+
};
|
|
548
|
+
this.instruments.valueStateCounter.add(1, stateAttrs);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (upstreamCause && upstreamCause.causes.length > 0) {
|
|
552
|
+
for (const cause of upstreamCause.causes) {
|
|
553
|
+
this.instruments.downstreamCauseCounter.add(1, {
|
|
554
|
+
...baseAttrs,
|
|
555
|
+
cause_root: cause.service,
|
|
556
|
+
cause_signal: cause.signal
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (this.traceApi) {
|
|
561
|
+
const span = this.traceApi.getActiveSpan?.();
|
|
562
|
+
maybeAttachToSpan(span, score, this.events, upstreamCause);
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
dispose() {
|
|
568
|
+
this._state = "disposed";
|
|
569
|
+
this.instruments = null;
|
|
570
|
+
this.traceApi = null;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var cachedApi2;
|
|
574
|
+
async function tryLoadOtelApi() {
|
|
575
|
+
if (cachedApi2 !== void 0) return cachedApi2;
|
|
576
|
+
try {
|
|
577
|
+
cachedApi2 = await import("@opentelemetry/api");
|
|
578
|
+
} catch {
|
|
579
|
+
cachedApi2 = null;
|
|
580
|
+
}
|
|
581
|
+
return cachedApi2;
|
|
582
|
+
}
|
|
583
|
+
function buildInstruments(meter) {
|
|
584
|
+
return {
|
|
585
|
+
scoreGauge: meter.createGauge(METRIC_NAMES.score, {
|
|
586
|
+
description: "Multi-signal health score per ADR-141 \xA74. Range [0, 1]. Higher = healthier.",
|
|
587
|
+
unit: "1"
|
|
588
|
+
}),
|
|
589
|
+
factorGauge: meter.createGauge(METRIC_NAMES.signalFactor, {
|
|
590
|
+
description: "Per-signal health factor; folded into health.score by the SDK scoring strategy (default: weightedProduct, ADR-111 \xA74.3).",
|
|
591
|
+
unit: "1"
|
|
592
|
+
}),
|
|
593
|
+
valueGauge: meter.createGauge(METRIC_NAMES.signalValue, {
|
|
594
|
+
description: "Raw observation behind a health signal (ms, count, rate). Numeric values only; string-valued signals emit health.signal.value_state instead."
|
|
595
|
+
}),
|
|
596
|
+
valueStateCounter: meter.createCounter(METRIC_NAMES.signalValueState, {
|
|
597
|
+
description: 'Per-evaluation counter for non-numeric signal readings (e.g. "ok" / "timeout"). state attribute carries the string label.'
|
|
598
|
+
}),
|
|
599
|
+
downstreamCauseCounter: meter.createCounter(METRIC_NAMES.downstreamCause, {
|
|
600
|
+
description: "Per-evaluation counter incremented when this service observes an upstream cause via OTel baggage (ADR-143). Operators read rate() to size the blast radius of an incident."
|
|
601
|
+
})
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function maybeAttachToSpan(span, score, events, upstreamCause) {
|
|
605
|
+
if (!span || typeof span !== "object") return;
|
|
606
|
+
const setAttribute = span.setAttribute;
|
|
607
|
+
if (typeof setAttribute === "function") {
|
|
608
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.score, score.score);
|
|
609
|
+
for (const [signalName, factor] of Object.entries(score.signalFactors ?? {})) {
|
|
610
|
+
if (typeof factor === "number" && Number.isFinite(factor)) {
|
|
611
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.signalFactor(signalName), factor);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (upstreamCause && upstreamCause.causes.length > 0) {
|
|
615
|
+
const primary = upstreamCause.causes[0];
|
|
616
|
+
if (primary) {
|
|
617
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.causeRoot, primary.service);
|
|
618
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.causeSignal, primary.signal);
|
|
619
|
+
}
|
|
620
|
+
if (upstreamCause.chain.length > 0) {
|
|
621
|
+
setAttribute.call(span, SPAN_ATTRIBUTES.causeChain, upstreamCause.chain.join(","));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (events) {
|
|
626
|
+
const addEvent = span.addEvent;
|
|
627
|
+
if (typeof addEvent === "function") {
|
|
628
|
+
addEvent.call(span, EVENT_NAMES.evaluated, {
|
|
629
|
+
[SPAN_ATTRIBUTES.score]: score.score,
|
|
630
|
+
"service.health.lastTickAt": score.lastTickAt
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
73
636
|
// src/health/scoring.ts
|
|
74
637
|
function clamp01(x) {
|
|
75
638
|
if (!Number.isFinite(x)) return 0;
|
|
@@ -195,6 +758,168 @@ function startUnixSocketServer(source, opts = {}) {
|
|
|
195
758
|
return handle;
|
|
196
759
|
}
|
|
197
760
|
|
|
761
|
+
// src/health/middleware.ts
|
|
762
|
+
var DEFAULT_PATH = "/healthz";
|
|
763
|
+
var DEFAULT_SOCKET_PATH = "/var/run/sylphx/health.sock";
|
|
764
|
+
var AUTO_SOCKET_HANDLES = /* @__PURE__ */ new WeakMap();
|
|
765
|
+
function resolveInstance(opts) {
|
|
766
|
+
if (opts.instance) return opts.instance;
|
|
767
|
+
const { path: _path, instance: _instance, unixSocket: _unixSocket, ...sylphxOpts } = opts;
|
|
768
|
+
return sylphxHealth(sylphxOpts);
|
|
769
|
+
}
|
|
770
|
+
function normalizePath(input) {
|
|
771
|
+
if (!input) return DEFAULT_PATH;
|
|
772
|
+
return input.startsWith("/") ? input : `/${input}`;
|
|
773
|
+
}
|
|
774
|
+
function getEnvSocketPath() {
|
|
775
|
+
const proc = globalThis.process;
|
|
776
|
+
const value = proc?.env?.SYLPHX_HEALTH_APP_SOCKET;
|
|
777
|
+
return value && value.trim().length > 0 ? value.trim() : void 0;
|
|
778
|
+
}
|
|
779
|
+
function resolveUnixSocketPath(inputPath, envPath) {
|
|
780
|
+
if (inputPath !== void 0) return inputPath;
|
|
781
|
+
if (envPath !== void 0) return envPath;
|
|
782
|
+
return DEFAULT_SOCKET_PATH;
|
|
783
|
+
}
|
|
784
|
+
function resolveUnixSocketOption(input) {
|
|
785
|
+
if (input === false) return null;
|
|
786
|
+
const envPath = getEnvSocketPath();
|
|
787
|
+
if (input === void 0) {
|
|
788
|
+
return envPath ? { path: envPath, required: false } : null;
|
|
789
|
+
}
|
|
790
|
+
if (input === true) {
|
|
791
|
+
return { path: envPath ?? DEFAULT_SOCKET_PATH, required: false };
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
path: resolveUnixSocketPath(input.path, envPath),
|
|
795
|
+
required: input.required ?? false,
|
|
796
|
+
...input.unlinkOnShutdown !== void 0 ? { unlinkOnShutdown: input.unlinkOnShutdown } : {}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function bindUnixSocketIfRequested(health, opts) {
|
|
800
|
+
const socket = resolveUnixSocketOption(opts.unixSocket);
|
|
801
|
+
if (!socket) return () => void 0;
|
|
802
|
+
const existing = AUTO_SOCKET_HANDLES.get(health);
|
|
803
|
+
if (existing) return () => void 0;
|
|
804
|
+
try {
|
|
805
|
+
const handle = health.serveUnixSocket({
|
|
806
|
+
path: socket.path,
|
|
807
|
+
...socket.unlinkOnShutdown !== void 0 ? { unlinkOnShutdown: socket.unlinkOnShutdown } : {}
|
|
808
|
+
});
|
|
809
|
+
AUTO_SOCKET_HANDLES.set(health, handle);
|
|
810
|
+
return () => {
|
|
811
|
+
if (AUTO_SOCKET_HANDLES.get(health) === handle) {
|
|
812
|
+
AUTO_SOCKET_HANDLES.delete(health);
|
|
813
|
+
}
|
|
814
|
+
void handle.shutdown();
|
|
815
|
+
};
|
|
816
|
+
} catch (err) {
|
|
817
|
+
if (socket.required) throw err;
|
|
818
|
+
return () => void 0;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function honoHelper(opts = {}) {
|
|
822
|
+
const path = normalizePath(opts.path);
|
|
823
|
+
const health = resolveInstance(opts);
|
|
824
|
+
const disposeSocket = bindUnixSocketIfRequested(health, opts);
|
|
825
|
+
const respond = health.handler();
|
|
826
|
+
const middleware = (async (c, next) => {
|
|
827
|
+
if (c.req.path === path) return respond();
|
|
828
|
+
await health.runWithCause(() => next());
|
|
829
|
+
return void 0;
|
|
830
|
+
});
|
|
831
|
+
Object.defineProperty(middleware, "health", { value: health, enumerable: true });
|
|
832
|
+
Object.defineProperty(middleware, "dispose", {
|
|
833
|
+
value: () => {
|
|
834
|
+
disposeSocket();
|
|
835
|
+
health.dispose();
|
|
836
|
+
},
|
|
837
|
+
enumerable: false
|
|
838
|
+
});
|
|
839
|
+
return middleware;
|
|
840
|
+
}
|
|
841
|
+
function pathOf(url) {
|
|
842
|
+
if (!url) return "/";
|
|
843
|
+
const q = url.indexOf("?");
|
|
844
|
+
return q === -1 ? url : url.slice(0, q);
|
|
845
|
+
}
|
|
846
|
+
function expressHelper(opts = {}) {
|
|
847
|
+
const path = normalizePath(opts.path);
|
|
848
|
+
const health = resolveInstance(opts);
|
|
849
|
+
const disposeSocket = bindUnixSocketIfRequested(health, opts);
|
|
850
|
+
const respond = health.nodeHandler();
|
|
851
|
+
const middleware = ((req, res, next) => {
|
|
852
|
+
if (pathOf(req.url) === path) {
|
|
853
|
+
void respond(req, res);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
void health.runWithCause(async () => next());
|
|
857
|
+
});
|
|
858
|
+
Object.defineProperty(middleware, "health", { value: health, enumerable: true });
|
|
859
|
+
Object.defineProperty(middleware, "dispose", {
|
|
860
|
+
value: () => {
|
|
861
|
+
disposeSocket();
|
|
862
|
+
health.dispose();
|
|
863
|
+
},
|
|
864
|
+
enumerable: false
|
|
865
|
+
});
|
|
866
|
+
return middleware;
|
|
867
|
+
}
|
|
868
|
+
function fastifyHelper(opts = {}) {
|
|
869
|
+
const path = normalizePath(opts.path);
|
|
870
|
+
const health = resolveInstance(opts);
|
|
871
|
+
const disposeSocket = bindUnixSocketIfRequested(health, opts);
|
|
872
|
+
const respond = health.handler();
|
|
873
|
+
const plugin = (async (instance) => {
|
|
874
|
+
instance.get(path, async (_req, reply) => {
|
|
875
|
+
const response = await respond();
|
|
876
|
+
const body = await response.text();
|
|
877
|
+
reply.code(response.status);
|
|
878
|
+
reply.header("Content-Type", "application/json");
|
|
879
|
+
reply.header("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
880
|
+
reply.send(body);
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
Object.defineProperty(plugin, "health", { value: health, enumerable: true });
|
|
884
|
+
Object.defineProperty(plugin, "dispose", {
|
|
885
|
+
value: () => {
|
|
886
|
+
disposeSocket();
|
|
887
|
+
health.dispose();
|
|
888
|
+
},
|
|
889
|
+
enumerable: false
|
|
890
|
+
});
|
|
891
|
+
return plugin;
|
|
892
|
+
}
|
|
893
|
+
function fetchHelper(opts = {}) {
|
|
894
|
+
const health = resolveInstance(opts);
|
|
895
|
+
const disposeSocket = bindUnixSocketIfRequested(health, opts);
|
|
896
|
+
const respond = health.handler();
|
|
897
|
+
const handler = ((req) => respond(req));
|
|
898
|
+
Object.defineProperty(handler, "health", { value: health, enumerable: true });
|
|
899
|
+
Object.defineProperty(handler, "dispose", {
|
|
900
|
+
value: () => {
|
|
901
|
+
disposeSocket();
|
|
902
|
+
health.dispose();
|
|
903
|
+
},
|
|
904
|
+
enumerable: false
|
|
905
|
+
});
|
|
906
|
+
return handler;
|
|
907
|
+
}
|
|
908
|
+
var withHealth = {
|
|
909
|
+
/** Hono middleware. Mount with `app.use('*', withHealth.hono())`. */
|
|
910
|
+
hono: honoHelper,
|
|
911
|
+
/** Express / Connect / Polka middleware. Mount with `app.use(withHealth.express())`. */
|
|
912
|
+
express: expressHelper,
|
|
913
|
+
/** Fastify plugin. Register with `await fastify.register(withHealth.fastify())`. */
|
|
914
|
+
fastify: fastifyHelper,
|
|
915
|
+
/**
|
|
916
|
+
* Pure Web Fetch handler. Works with Bun.serve, Next.js route
|
|
917
|
+
* handlers, Deno, Cloudflare Workers, and any other `Request →
|
|
918
|
+
* Response` runtime.
|
|
919
|
+
*/
|
|
920
|
+
fetch: fetchHelper
|
|
921
|
+
};
|
|
922
|
+
|
|
198
923
|
// src/health/signals/error-rate.ts
|
|
199
924
|
function parseWindow(w) {
|
|
200
925
|
if (typeof w === "number") {
|
|
@@ -362,6 +1087,60 @@ function memoryPressureSignal(opts = {}) {
|
|
|
362
1087
|
};
|
|
363
1088
|
}
|
|
364
1089
|
|
|
1090
|
+
// src/health/signals/ping.ts
|
|
1091
|
+
var DEFAULT_TIMEOUT_MS = 500;
|
|
1092
|
+
function pingSignal(opts) {
|
|
1093
|
+
if (typeof opts.ping !== "function") {
|
|
1094
|
+
throw new Error(`pingSignal[${opts.name}]: ping must be a function`);
|
|
1095
|
+
}
|
|
1096
|
+
if (typeof opts.name !== "string" || opts.name.length === 0) {
|
|
1097
|
+
throw new Error("pingSignal: name must be a non-empty string");
|
|
1098
|
+
}
|
|
1099
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1100
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1101
|
+
throw new Error(`pingSignal[${opts.name}]: timeoutMs must be > 0, got ${timeoutMs}`);
|
|
1102
|
+
}
|
|
1103
|
+
const weight = opts.weight ?? 1;
|
|
1104
|
+
return {
|
|
1105
|
+
name: opts.name,
|
|
1106
|
+
weight,
|
|
1107
|
+
async read() {
|
|
1108
|
+
let timer;
|
|
1109
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1110
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
1111
|
+
});
|
|
1112
|
+
try {
|
|
1113
|
+
const result = await Promise.race([opts.ping().then(() => "ok"), timeoutPromise]);
|
|
1114
|
+
if (result === "timeout") {
|
|
1115
|
+
return { value: "timeout", healthFactor: 0 };
|
|
1116
|
+
}
|
|
1117
|
+
return { value: "ok", healthFactor: 1 };
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1120
|
+
return { value: message.slice(0, 120), healthFactor: 0 };
|
|
1121
|
+
} finally {
|
|
1122
|
+
if (timer) clearTimeout(timer);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function databaseSignal(opts) {
|
|
1128
|
+
return pingSignal({
|
|
1129
|
+
name: opts.name ?? "database",
|
|
1130
|
+
ping: opts.ping,
|
|
1131
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
1132
|
+
...opts.weight !== void 0 ? { weight: opts.weight } : {}
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
function redisSignal(opts) {
|
|
1136
|
+
return pingSignal({
|
|
1137
|
+
name: opts.name ?? "redis",
|
|
1138
|
+
ping: opts.ping,
|
|
1139
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
1140
|
+
...opts.weight !== void 0 ? { weight: opts.weight } : {}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
365
1144
|
// src/health/signals/queue-depth.ts
|
|
366
1145
|
function queueDepthSignal(opts) {
|
|
367
1146
|
if (typeof opts.getter !== "function") {
|
|
@@ -407,15 +1186,41 @@ function queueDepthSignal(opts) {
|
|
|
407
1186
|
}
|
|
408
1187
|
|
|
409
1188
|
// src/health/index.ts
|
|
1189
|
+
function resolveOtelEmitter(otelOpt) {
|
|
1190
|
+
if (otelOpt === false) return null;
|
|
1191
|
+
const emitterOpts = otelOpt && typeof otelOpt === "object" ? otelOpt : {};
|
|
1192
|
+
return createOtelEmitter(emitterOpts);
|
|
1193
|
+
}
|
|
1194
|
+
function resolveCausalityHandle(opt) {
|
|
1195
|
+
if (opt === false) return null;
|
|
1196
|
+
const causalityOpts = opt && typeof opt === "object" ? opt : {};
|
|
1197
|
+
return createCausalityHandle(causalityOpts);
|
|
1198
|
+
}
|
|
1199
|
+
function resolveHistoryRecorder(opt) {
|
|
1200
|
+
if (opt === false) return null;
|
|
1201
|
+
const recorderOpts = opt && typeof opt === "object" ? opt : {};
|
|
1202
|
+
return createHistoryRecorder(recorderOpts);
|
|
1203
|
+
}
|
|
1204
|
+
function clampHealthFactor(value) {
|
|
1205
|
+
if (!Number.isFinite(value)) return 0;
|
|
1206
|
+
if (value < 0) return 0;
|
|
1207
|
+
if (value > 1) return 1;
|
|
1208
|
+
return value;
|
|
1209
|
+
}
|
|
410
1210
|
function sylphxHealth(opts = {}) {
|
|
411
1211
|
const signals = opts.signals && opts.signals.length > 0 ? [...opts.signals] : [eventLoopLagSignal({ degradedMs: 5e3, deadMs: 3e4, weight: 1 })];
|
|
412
1212
|
const scoringStrategy = opts.scoringStrategy ?? defaultScoringStrategy();
|
|
413
1213
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
1214
|
+
const otelEmitter = resolveOtelEmitter(opts.otel);
|
|
1215
|
+
const causality = resolveCausalityHandle(opts.causality);
|
|
1216
|
+
const historyRecorder = resolveHistoryRecorder(opts.history);
|
|
414
1217
|
let disposed = false;
|
|
1218
|
+
let lastScore = null;
|
|
415
1219
|
const evaluate = async () => {
|
|
416
1220
|
if (disposed) {
|
|
417
1221
|
throw new Error("sylphxHealth: instance disposed");
|
|
418
1222
|
}
|
|
1223
|
+
const upstreamCause = causality ? causality.readCause() : null;
|
|
419
1224
|
const readings = await Promise.all(
|
|
420
1225
|
signals.map(async (signal) => ({
|
|
421
1226
|
signal,
|
|
@@ -424,17 +1229,47 @@ function sylphxHealth(opts = {}) {
|
|
|
424
1229
|
);
|
|
425
1230
|
const score = scoringStrategy(readings);
|
|
426
1231
|
const signalsMap = {};
|
|
1232
|
+
const signalFactors = {};
|
|
427
1233
|
for (const { signal, reading } of readings) {
|
|
428
1234
|
signalsMap[signal.name] = reading.value;
|
|
1235
|
+
signalFactors[signal.name] = reading.unknown === true ? 1 : clampHealthFactor(reading.healthFactor);
|
|
429
1236
|
}
|
|
430
|
-
|
|
1237
|
+
const result = {
|
|
431
1238
|
score,
|
|
432
1239
|
signals: signalsMap,
|
|
1240
|
+
signalFactors,
|
|
433
1241
|
lastTickAt: now().toISOString()
|
|
434
1242
|
};
|
|
1243
|
+
lastScore = result;
|
|
1244
|
+
if (otelEmitter) {
|
|
1245
|
+
otelEmitter.emit(result, upstreamCause);
|
|
1246
|
+
}
|
|
1247
|
+
if (historyRecorder) {
|
|
1248
|
+
void historyRecorder.record(result);
|
|
1249
|
+
}
|
|
1250
|
+
return result;
|
|
435
1251
|
};
|
|
436
1252
|
const evaluator = { evaluate };
|
|
437
1253
|
const evaluateEffect2 = evaluateEffect(evaluate);
|
|
1254
|
+
const runWithCause = async (fn) => {
|
|
1255
|
+
if (!causality || !lastScore) return await fn();
|
|
1256
|
+
return await causality.runWithCause(fn, lastScore);
|
|
1257
|
+
};
|
|
1258
|
+
const getHistory = async (query = {}) => {
|
|
1259
|
+
if (!historyRecorder) return [];
|
|
1260
|
+
return await historyRecorder.range(query);
|
|
1261
|
+
};
|
|
1262
|
+
const verifyHistory = async (query = {}) => {
|
|
1263
|
+
if (!historyRecorder) {
|
|
1264
|
+
return {
|
|
1265
|
+
verified: false,
|
|
1266
|
+
entriesChecked: 0,
|
|
1267
|
+
firstBreakAtSequence: null,
|
|
1268
|
+
reason: "history recording is disabled on this instance"
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
return await historyRecorder.verify(query);
|
|
1272
|
+
};
|
|
438
1273
|
return {
|
|
439
1274
|
signals,
|
|
440
1275
|
evaluate,
|
|
@@ -448,9 +1283,18 @@ function sylphxHealth(opts = {}) {
|
|
|
448
1283
|
serveUnixSocket(unixOpts) {
|
|
449
1284
|
return startUnixSocketServer(evaluator, unixOpts);
|
|
450
1285
|
},
|
|
1286
|
+
getLastScore() {
|
|
1287
|
+
return lastScore;
|
|
1288
|
+
},
|
|
1289
|
+
runWithCause,
|
|
1290
|
+
getHistory,
|
|
1291
|
+
verifyHistory,
|
|
451
1292
|
dispose() {
|
|
452
1293
|
if (disposed) return;
|
|
453
1294
|
disposed = true;
|
|
1295
|
+
if (otelEmitter) otelEmitter.dispose();
|
|
1296
|
+
if (causality) causality.dispose();
|
|
1297
|
+
if (historyRecorder) historyRecorder.dispose();
|
|
454
1298
|
for (const s of signals) {
|
|
455
1299
|
try {
|
|
456
1300
|
s.dispose?.();
|
|
@@ -461,15 +1305,34 @@ function sylphxHealth(opts = {}) {
|
|
|
461
1305
|
};
|
|
462
1306
|
}
|
|
463
1307
|
export {
|
|
1308
|
+
BAGGAGE_KEY_CAUSE,
|
|
1309
|
+
BAGGAGE_KEY_CHAIN,
|
|
1310
|
+
DEFAULT_CAUSE_THRESHOLD,
|
|
1311
|
+
DEFAULT_HISTORY_CAPACITY,
|
|
1312
|
+
GENESIS_PREV_HASH,
|
|
464
1313
|
HealthError,
|
|
1314
|
+
MAX_CHAIN_LENGTH,
|
|
1315
|
+
EVENT_NAMES as OTEL_EVENT_NAMES,
|
|
1316
|
+
METRIC_NAMES as OTEL_METRIC_NAMES,
|
|
1317
|
+
SPAN_ATTRIBUTES as OTEL_SPAN_ATTRIBUTES,
|
|
1318
|
+
canonicalizeSignals,
|
|
1319
|
+
createCausalityHandle,
|
|
1320
|
+
createHistoryRecorder,
|
|
1321
|
+
createInMemoryHistoryStore,
|
|
465
1322
|
createNodeHandler,
|
|
1323
|
+
createOtelEmitter,
|
|
466
1324
|
createWebHandler,
|
|
1325
|
+
databaseSignal,
|
|
467
1326
|
defaultScoringStrategy,
|
|
468
1327
|
errorRateSignal,
|
|
469
1328
|
eventLoopLagSignal,
|
|
470
1329
|
memoryPressureSignal,
|
|
1330
|
+
pingSignal,
|
|
471
1331
|
queueDepthSignal,
|
|
1332
|
+
redisSignal,
|
|
472
1333
|
sylphxHealth,
|
|
473
|
-
|
|
1334
|
+
verifyChain,
|
|
1335
|
+
weightedProduct,
|
|
1336
|
+
withHealth
|
|
474
1337
|
};
|
|
475
1338
|
//# sourceMappingURL=index.mjs.map
|