okengine 0.9.1 → 0.10.1
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/AGENTS.md +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +97 -9
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-app-runner.ts +6 -0
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +633 -66
- package/src/cli/docker-cli.test.ts +15 -0
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/cli/start.ts +12 -1
- package/src/compiler/effects-infer.ts +37 -8
- package/src/compiler/extract.test.ts +125 -0
- package/src/compiler/extract.ts +31 -6
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.test.ts +28 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +131 -10
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +113 -10
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/effects-stamping.test.ts +207 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +153 -14
- package/src/manifest/sql-resource.ts +41 -0
- package/src/manifest/types.ts +8 -2
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL resource-ref naming — the one place both the compiler's AST-based
|
|
3
|
+
* effect inference ({@link "../compiler/effects-infer.ts"}) and the
|
|
4
|
+
* kernel's capability gate ({@link "../kernel/fx.ts"} `gatedSqlHandle`)
|
|
5
|
+
* compute a per-table `sql:<table>` ref, so the two cannot silently
|
|
6
|
+
* drift apart again.
|
|
7
|
+
*
|
|
8
|
+
* Table-name *resolution* differs by nature — the compiler reads a static
|
|
9
|
+
* AST identifier (or, better, the declared name behind a
|
|
10
|
+
* `store.schema.table(name, …)` binding); the kernel reads the declared
|
|
11
|
+
* name straight off the live table object at call time. Only the
|
|
12
|
+
* naming/formatting step is shared here; that is the part a second,
|
|
13
|
+
* independently-written implementation could get wrong.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ResourceRef } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The declared name a real `store.schema.table(name, columns)` object
|
|
20
|
+
* carries at runtime — the same string that was the first argument to
|
|
21
|
+
* `store.schema.table(...)` in source, regardless of what the JS binding
|
|
22
|
+
* is called. `undefined` for anything else (raw ORM tables, plain
|
|
23
|
+
* objects, missing/optional table arguments).
|
|
24
|
+
*
|
|
25
|
+
* @param value - Value passed where a table argument is expected
|
|
26
|
+
*/
|
|
27
|
+
export function schemaTableName(value: unknown): string | undefined {
|
|
28
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
29
|
+
const v = value as { kind?: unknown; name?: unknown };
|
|
30
|
+
if (v.kind !== "schema-table") return undefined;
|
|
31
|
+
return typeof v.name === "string" ? v.name : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `sql:<table>` resource ref for a declared table name.
|
|
36
|
+
*
|
|
37
|
+
* @param table - Declared table name
|
|
38
|
+
*/
|
|
39
|
+
export function sqlTableRef(table: string): ResourceRef {
|
|
40
|
+
return `sql:${table}` as ResourceRef;
|
|
41
|
+
}
|
package/src/manifest/types.ts
CHANGED
|
@@ -40,6 +40,12 @@ export type ChannelMedium = "email" | "sms" | "whatsapp" | "push" | "any";
|
|
|
40
40
|
/** Resource ref: `sql:table`, `kv:namespace`, `files:bucket`, `index:name`. */
|
|
41
41
|
export type ResourceRef = `${StoreFacet}:${string}`;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Observability read capability for `fx.runs` — not a store facet.
|
|
45
|
+
* Declare on `effects.reads` (never `writes`).
|
|
46
|
+
*/
|
|
47
|
+
export type RunsResourceRef = "runs";
|
|
48
|
+
|
|
43
49
|
/** Signal name reference. */
|
|
44
50
|
export type SignalRef = string;
|
|
45
51
|
|
|
@@ -68,8 +74,8 @@ export type JsonSchema = string | Record<string, unknown>;
|
|
|
68
74
|
* `sends` / `asks` are irreversible (asks also nondeterministic + cost).
|
|
69
75
|
*/
|
|
70
76
|
export interface Effects {
|
|
71
|
-
/** Store reads. */
|
|
72
|
-
reads?: ResourceRef
|
|
77
|
+
/** Store reads, plus optional `"runs"` for `fx.runs`. */
|
|
78
|
+
reads?: Array<ResourceRef | RunsResourceRef>;
|
|
73
79
|
/** Store writes. */
|
|
74
80
|
writes?: ResourceRef[];
|
|
75
81
|
/** Emitted signals. */
|
package/src/runs/collect.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface CollectWideEventInput {
|
|
|
32
32
|
readonly endedAt: number;
|
|
33
33
|
/** Pipeline failure when present. */
|
|
34
34
|
readonly failure?: FlowFailure | null;
|
|
35
|
+
/** Validated flow input (for local replay). */
|
|
36
|
+
readonly input?: unknown;
|
|
35
37
|
/** Optional parent run id (causal chain). */
|
|
36
38
|
readonly parentId?: string;
|
|
37
39
|
/** Optional run id (generated when omitted). */
|
|
@@ -104,6 +106,7 @@ export function collectWideEvent(input: CollectWideEventInput): WideEvent {
|
|
|
104
106
|
: {}),
|
|
105
107
|
...(input.buildVersion !== undefined ? { buildVersion: input.buildVersion } : {}),
|
|
106
108
|
error,
|
|
109
|
+
...(input.input !== undefined ? { input: input.input } : {}),
|
|
107
110
|
effects: [...input.ledger.entries],
|
|
108
111
|
logs: [...input.telemetry.logs],
|
|
109
112
|
durationMs,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional OTLP metrics export shape — additive sink for WideEvents.
|
|
3
|
+
*
|
|
4
|
+
* Not a prerequisite for native alerting (Runs + Clock + Channel).
|
|
5
|
+
* Protocol-named (`otlp`); vendor choice stays in config / images.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WideEvent } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
/** One OTLP-shaped metric datapoint derived from a wide event. */
|
|
11
|
+
export interface OtlpRunMetric {
|
|
12
|
+
readonly name: "oke.runs" | "oke.run.duration_ms";
|
|
13
|
+
readonly kind: "counter" | "histogram";
|
|
14
|
+
readonly value: number;
|
|
15
|
+
readonly attributes: Readonly<Record<string, string>>;
|
|
16
|
+
readonly timeUnixMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Map a WideEvent onto OTLP-friendly metric datapoints.
|
|
21
|
+
*
|
|
22
|
+
* @param event - Appended run
|
|
23
|
+
*/
|
|
24
|
+
export function wideEventToOtlpMetrics(event: WideEvent): readonly OtlpRunMetric[] {
|
|
25
|
+
const attributes = {
|
|
26
|
+
flow: event.flow,
|
|
27
|
+
trigger: event.trigger,
|
|
28
|
+
outcome: event.error ? "error" : "ok",
|
|
29
|
+
...(event.error ? { error_code: event.error.code } : {}),
|
|
30
|
+
};
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
name: "oke.runs",
|
|
34
|
+
kind: "counter",
|
|
35
|
+
value: 1,
|
|
36
|
+
attributes,
|
|
37
|
+
timeUnixMs: event.endedAt,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "oke.run.duration_ms",
|
|
41
|
+
kind: "histogram",
|
|
42
|
+
value: event.durationMs,
|
|
43
|
+
attributes: { flow: event.flow, trigger: event.trigger },
|
|
44
|
+
timeUnixMs: event.endedAt,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Pluggable sink for teams with an existing Grafana / Datadog stack. */
|
|
50
|
+
export type OtlpMetricsSink = (metrics: readonly OtlpRunMetric[]) => void | Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fan-out helper: call after `runs.record` when an OTLP sink is configured.
|
|
54
|
+
*
|
|
55
|
+
* @param event - Just-recorded wide event
|
|
56
|
+
* @param sink - Optional exporter
|
|
57
|
+
*/
|
|
58
|
+
export async function exportRunMetrics(
|
|
59
|
+
event: WideEvent,
|
|
60
|
+
sink: OtlpMetricsSink | undefined,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
if (!sink) return;
|
|
63
|
+
await sink(wideEventToOtlpMetrics(event));
|
|
64
|
+
}
|
package/src/runs/index.ts
CHANGED
|
@@ -68,5 +68,21 @@ export {
|
|
|
68
68
|
|
|
69
69
|
export { privacyErase, type PrivacyEraseOptions, type PrivacyEraseResult } from "./privacy.ts";
|
|
70
70
|
|
|
71
|
+
export {
|
|
72
|
+
evaluateSloBreaches,
|
|
73
|
+
parseLatencyMs,
|
|
74
|
+
percentile,
|
|
75
|
+
windowStatsForFlow,
|
|
76
|
+
type RunWindowStats,
|
|
77
|
+
type SloBreach,
|
|
78
|
+
} from "./window.ts";
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
exportRunMetrics,
|
|
82
|
+
wideEventToOtlpMetrics,
|
|
83
|
+
type OtlpMetricsSink,
|
|
84
|
+
type OtlpRunMetric,
|
|
85
|
+
} from "./export-otlp.ts";
|
|
86
|
+
|
|
71
87
|
export { createRunsPostgresFake } from "./drivers/postgres.ts";
|
|
72
88
|
export { createRunsClickHouseFake } from "./drivers/clickhouse.ts";
|
package/src/runs/parquet.ts
CHANGED
|
@@ -42,6 +42,7 @@ export function wideEventToRow(event: WideEvent): ParquetRow {
|
|
|
42
42
|
build_version: event.buildVersion ?? null,
|
|
43
43
|
error_code: event.error?.code ?? null,
|
|
44
44
|
error_message: event.error?.message ?? null,
|
|
45
|
+
input: event.input === undefined ? null : JSON.stringify(event.input),
|
|
45
46
|
effects: JSON.stringify(event.effects),
|
|
46
47
|
logs: JSON.stringify(event.logs),
|
|
47
48
|
duration_ms: event.durationMs,
|
|
@@ -103,6 +104,14 @@ export function rowToWideEvent(row: Record<string, unknown>): WideEvent {
|
|
|
103
104
|
...(row.error_message != null ? { message: String(row.error_message) } : {}),
|
|
104
105
|
}
|
|
105
106
|
: null,
|
|
107
|
+
...(row.input != null && row.input !== ""
|
|
108
|
+
? {
|
|
109
|
+
input:
|
|
110
|
+
typeof row.input === "string"
|
|
111
|
+
? (JSON.parse(row.input) as unknown)
|
|
112
|
+
: (row.input as unknown),
|
|
113
|
+
}
|
|
114
|
+
: {}),
|
|
106
115
|
effects: parseJsonArray(row.effects) as WideEvent["effects"],
|
|
107
116
|
logs: parseJsonArray(row.logs) as WideEvent["logs"],
|
|
108
117
|
durationMs: Number(row.duration_ms ?? 0),
|
package/src/runs/types.ts
CHANGED
|
@@ -69,6 +69,11 @@ export interface WideEvent {
|
|
|
69
69
|
readonly buildVersion?: string;
|
|
70
70
|
/** Typed error when the flow failed. */
|
|
71
71
|
readonly error?: RunError | null;
|
|
72
|
+
/**
|
|
73
|
+
* Validated flow input snapshot for local replay (`oke replay`).
|
|
74
|
+
* Declared personal fields may be crypto-shredded into {@link archived}.
|
|
75
|
+
*/
|
|
76
|
+
readonly input?: unknown;
|
|
72
77
|
/** Effect ledger snapshot. */
|
|
73
78
|
readonly effects: readonly EffectEntry[];
|
|
74
79
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { WideEvent } from "./types.ts";
|
|
3
|
+
import { evaluateSloBreaches, parseLatencyMs, windowStatsForFlow } from "./window.ts";
|
|
4
|
+
|
|
5
|
+
function ev(
|
|
6
|
+
partial: Partial<WideEvent> & Pick<WideEvent, "id" | "durationMs" | "startedAt">,
|
|
7
|
+
): WideEvent {
|
|
8
|
+
return {
|
|
9
|
+
flow: "checkout.create",
|
|
10
|
+
trigger: "http",
|
|
11
|
+
plane: "user",
|
|
12
|
+
gates: [],
|
|
13
|
+
cache: "none",
|
|
14
|
+
error: null,
|
|
15
|
+
effects: [],
|
|
16
|
+
logs: [],
|
|
17
|
+
endedAt: partial.startedAt + partial.durationMs,
|
|
18
|
+
dimensions: {},
|
|
19
|
+
...partial,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("runs window stats", () => {
|
|
24
|
+
test("parseLatencyMs", () => {
|
|
25
|
+
expect(parseLatencyMs("200ms")).toBe(200);
|
|
26
|
+
expect(parseLatencyMs("1s")).toBe(1000);
|
|
27
|
+
expect(parseLatencyMs("bad")).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("computes p95 and error rate", () => {
|
|
31
|
+
const now = 10_000;
|
|
32
|
+
const events = [
|
|
33
|
+
ev({ id: "1", durationMs: 10, startedAt: now - 1000 }),
|
|
34
|
+
ev({ id: "2", durationMs: 20, startedAt: now - 900 }),
|
|
35
|
+
ev({ id: "3", durationMs: 30, startedAt: now - 800 }),
|
|
36
|
+
ev({ id: "4", durationMs: 40, startedAt: now - 700 }),
|
|
37
|
+
ev({
|
|
38
|
+
id: "5",
|
|
39
|
+
durationMs: 200,
|
|
40
|
+
startedAt: now - 600,
|
|
41
|
+
error: { code: "Timeout" },
|
|
42
|
+
}),
|
|
43
|
+
];
|
|
44
|
+
const stats = windowStatsForFlow(events, "checkout.create", now, 5 * 60_000);
|
|
45
|
+
expect(stats.total).toBe(5);
|
|
46
|
+
expect(stats.errors).toBe(1);
|
|
47
|
+
expect(stats.errorRate).toBeCloseTo(0.2);
|
|
48
|
+
expect(stats.p95Ms).toBe(200);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("evaluateSloBreaches flags latency and availability", () => {
|
|
52
|
+
const stats = windowStatsForFlow(
|
|
53
|
+
[
|
|
54
|
+
ev({ id: "1", durationMs: 500, startedAt: 9000, error: { code: "X" } }),
|
|
55
|
+
ev({ id: "2", durationMs: 500, startedAt: 9100 }),
|
|
56
|
+
],
|
|
57
|
+
"checkout.create",
|
|
58
|
+
10_000,
|
|
59
|
+
5_000,
|
|
60
|
+
);
|
|
61
|
+
const breaches = evaluateSloBreaches(stats, {
|
|
62
|
+
availability: "99.9%",
|
|
63
|
+
latency: { p95: "100ms" },
|
|
64
|
+
});
|
|
65
|
+
expect(breaches.some((b) => b.kind === "availability")).toBe(true);
|
|
66
|
+
expect(breaches.some((b) => b.kind === "latency_p95")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rolling window metrics over WideEvents — shared by Console Overview and
|
|
3
|
+
* Flow-facing `fx.runs` checkers (native alerting without `fx.metric`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WideEvent } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/** One flow's rolling stats over a time window. */
|
|
9
|
+
export interface RunWindowStats {
|
|
10
|
+
readonly flow: string;
|
|
11
|
+
readonly total: number;
|
|
12
|
+
readonly errors: number;
|
|
13
|
+
readonly errorRate: number;
|
|
14
|
+
readonly p50Ms: number;
|
|
15
|
+
readonly p95Ms: number;
|
|
16
|
+
readonly p99Ms: number;
|
|
17
|
+
readonly from: number;
|
|
18
|
+
readonly to: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Percentile of a sorted numeric array.
|
|
23
|
+
*
|
|
24
|
+
* @param sorted - Ascending durations
|
|
25
|
+
* @param p - Percentile in (0, 1]
|
|
26
|
+
*/
|
|
27
|
+
export function percentile(sorted: readonly number[], p: number): number {
|
|
28
|
+
if (sorted.length === 0) return 0;
|
|
29
|
+
if (sorted.length === 1) return sorted[0]!;
|
|
30
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
|
|
31
|
+
return sorted[idx]!;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a Manifest latency string like `200ms` / `1s` into milliseconds.
|
|
36
|
+
*
|
|
37
|
+
* @param raw - Latency threshold
|
|
38
|
+
*/
|
|
39
|
+
export function parseLatencyMs(raw: string | undefined): number | null {
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m)?$/i.exec(raw.trim());
|
|
42
|
+
if (!match) return null;
|
|
43
|
+
const n = Number(match[1]);
|
|
44
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
45
|
+
const unit = (match[2] ?? "ms").toLowerCase();
|
|
46
|
+
if (unit === "s") return Math.round(n * 1000);
|
|
47
|
+
if (unit === "m") return Math.round(n * 60_000);
|
|
48
|
+
return Math.round(n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compute rolling stats for one flow over `[to - windowMs, to]`.
|
|
53
|
+
*
|
|
54
|
+
* @param events - Wide events
|
|
55
|
+
* @param flow - Flow name filter
|
|
56
|
+
* @param to - Window end (epoch-ms)
|
|
57
|
+
* @param windowMs - Window length
|
|
58
|
+
*/
|
|
59
|
+
export function windowStatsForFlow(
|
|
60
|
+
events: readonly WideEvent[],
|
|
61
|
+
flow: string,
|
|
62
|
+
to: number,
|
|
63
|
+
windowMs: number,
|
|
64
|
+
): RunWindowStats {
|
|
65
|
+
const from = to - Math.max(0, windowMs);
|
|
66
|
+
const mine = events.filter((e) => e.flow === flow && e.startedAt >= from && e.startedAt <= to);
|
|
67
|
+
const durations = mine.map((e) => e.durationMs).sort((a, b) => a - b);
|
|
68
|
+
const errors = mine.filter((e) => e.error != null).length;
|
|
69
|
+
const total = mine.length;
|
|
70
|
+
return {
|
|
71
|
+
flow,
|
|
72
|
+
total,
|
|
73
|
+
errors,
|
|
74
|
+
errorRate: total === 0 ? 0 : errors / total,
|
|
75
|
+
p50Ms: percentile(durations, 0.5),
|
|
76
|
+
p95Ms: percentile(durations, 0.95),
|
|
77
|
+
p99Ms: percentile(durations, 0.99),
|
|
78
|
+
from,
|
|
79
|
+
to,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Breach report for a declared latency / availability SLO. */
|
|
84
|
+
export interface SloBreach {
|
|
85
|
+
readonly flow: string;
|
|
86
|
+
readonly kind: "availability" | "latency_p95" | "latency_p99";
|
|
87
|
+
readonly threshold: number;
|
|
88
|
+
readonly observed: number;
|
|
89
|
+
readonly sampleCount: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Evaluate Manifest-style SLO thresholds against window stats.
|
|
94
|
+
*
|
|
95
|
+
* @param stats - Window stats
|
|
96
|
+
* @param slo - Declared availability + latency
|
|
97
|
+
*/
|
|
98
|
+
export function evaluateSloBreaches(
|
|
99
|
+
stats: RunWindowStats,
|
|
100
|
+
slo: {
|
|
101
|
+
readonly availability?: string;
|
|
102
|
+
readonly latency?: { readonly p95?: string; readonly p99?: string };
|
|
103
|
+
},
|
|
104
|
+
): SloBreach[] {
|
|
105
|
+
const out: SloBreach[] = [];
|
|
106
|
+
if (stats.total === 0) return out;
|
|
107
|
+
|
|
108
|
+
if (slo.availability) {
|
|
109
|
+
const match = /^(\d+(?:\.\d+)?)\s*%?$/.exec(slo.availability.trim());
|
|
110
|
+
if (match) {
|
|
111
|
+
const pct = Number(match[1]);
|
|
112
|
+
const tolerable = Math.max(0, 1 - pct / 100);
|
|
113
|
+
if (stats.errorRate > tolerable) {
|
|
114
|
+
out.push({
|
|
115
|
+
flow: stats.flow,
|
|
116
|
+
kind: "availability",
|
|
117
|
+
threshold: tolerable,
|
|
118
|
+
observed: stats.errorRate,
|
|
119
|
+
sampleCount: stats.total,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const p95 = parseLatencyMs(slo.latency?.p95);
|
|
126
|
+
if (p95 != null && stats.p95Ms > p95) {
|
|
127
|
+
out.push({
|
|
128
|
+
flow: stats.flow,
|
|
129
|
+
kind: "latency_p95",
|
|
130
|
+
threshold: p95,
|
|
131
|
+
observed: stats.p95Ms,
|
|
132
|
+
sampleCount: stats.total,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const p99 = parseLatencyMs(slo.latency?.p99);
|
|
137
|
+
if (p99 != null && stats.p99Ms > p99) {
|
|
138
|
+
out.push({
|
|
139
|
+
flow: stats.flow,
|
|
140
|
+
kind: "latency_p99",
|
|
141
|
+
threshold: p99,
|
|
142
|
+
observed: stats.p99Ms,
|
|
143
|
+
sampleCount: stats.total,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot honesty suppress flag for the `oke dev` Backend child.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { emitBootWarn } from "./boot-warn.ts";
|
|
7
|
+
|
|
8
|
+
describe("emitBootWarn", () => {
|
|
9
|
+
const prev = process.env["OKE_SUPPRESS_BOOT_WARN"];
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
if (prev === undefined) delete process.env["OKE_SUPPRESS_BOOT_WARN"];
|
|
13
|
+
else process.env["OKE_SUPPRESS_BOOT_WARN"] = prev;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("forwards to console.warn by default", () => {
|
|
17
|
+
delete process.env["OKE_SUPPRESS_BOOT_WARN"];
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
const original = console.warn;
|
|
20
|
+
console.warn = (...args: unknown[]) => {
|
|
21
|
+
lines.push(args.map(String).join(" "));
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
emitBootWarn("oke boot: test notice");
|
|
25
|
+
expect(lines).toEqual(["oke boot: test notice"]);
|
|
26
|
+
} finally {
|
|
27
|
+
console.warn = original;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("no-ops when OKE_SUPPRESS_BOOT_WARN=1", () => {
|
|
32
|
+
process.env["OKE_SUPPRESS_BOOT_WARN"] = "1";
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
const original = console.warn;
|
|
35
|
+
console.warn = (...args: unknown[]) => {
|
|
36
|
+
lines.push(args.map(String).join(" "));
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
emitBootWarn("oke boot: hidden");
|
|
40
|
+
expect(lines).toEqual([]);
|
|
41
|
+
} finally {
|
|
42
|
+
console.warn = original;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot honesty lines (`oke boot: …`) — once per process, suppressible in
|
|
3
|
+
* the `oke dev` Backend child so the parent Console owns the notice.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Emit a boot warning unless suppressed for this process.
|
|
8
|
+
*
|
|
9
|
+
* Set `OKE_SUPPRESS_BOOT_WARN=1` on the Backend child during `oke dev` so
|
|
10
|
+
* Signal / Channel / files process-local notices print once (parent only).
|
|
11
|
+
*
|
|
12
|
+
* @param message - Full warning text (usually prefixed `oke boot:`)
|
|
13
|
+
*/
|
|
14
|
+
export function emitBootWarn(message: string): void {
|
|
15
|
+
if (process.env["OKE_SUPPRESS_BOOT_WARN"] === "1") return;
|
|
16
|
+
console.warn(message);
|
|
17
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev-only request lines for `oke dev` (
|
|
2
|
+
* Dev-only request lines for `oke dev` (Backend / Console / MCP).
|
|
3
3
|
*
|
|
4
4
|
* Gated by `OKE_DEV_REQUEST_LOG=1`. Surfaces share one TTY; label + color
|
|
5
5
|
* keep streams readable without a multiplexer.
|
|
@@ -18,22 +18,22 @@ export function shouldLogDevRequests(): boolean {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Active surface for this async context, or `OKE_DEV_SURFACE`, or `
|
|
21
|
+
* Active surface for this async context, or `OKE_DEV_SURFACE`, or `Backend`.
|
|
22
22
|
*/
|
|
23
23
|
export function currentDevSurface(): DevLogSurface {
|
|
24
24
|
const fromAls = surfaceAls.getStore();
|
|
25
25
|
if (fromAls) return fromAls;
|
|
26
26
|
const fromEnv = process.env.OKE_DEV_SURFACE;
|
|
27
|
-
if (fromEnv === "
|
|
27
|
+
if (fromEnv === "Backend" || fromEnv === "Console" || fromEnv === "MCP") {
|
|
28
28
|
return fromEnv;
|
|
29
29
|
}
|
|
30
|
-
return "
|
|
30
|
+
return "Backend";
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Run a handler tagged with a surface (Console wrap around `app.fetch`).
|
|
35
35
|
*
|
|
36
|
-
* @param surface -
|
|
36
|
+
* @param surface - Backend · Console · MCP
|
|
37
37
|
* @param fn - Async work
|
|
38
38
|
*/
|
|
39
39
|
export function runWithDevSurface<T>(
|