@obsunified/collector 2.3.0 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +1 -1
- package/dist/{chunk-2FALTDCE.js → chunk-VSOZJO6Y.js} +12 -1
- package/dist/chunk-VSOZJO6Y.js.map +1 -0
- package/dist/index.d.ts +271 -62
- package/dist/index.js +1077 -137
- package/dist/index.js.map +1 -1
- package/dist/{ingest-auth-BmP5-f96.d.ts → ingest-auth-BTowieER.d.ts} +66 -2
- package/package.json +3 -3
- package/src/framework/collector.ts +22 -0
- package/src/framework/env.ts +9 -1
- package/src/framework/ingest-auth-warning.test.ts +71 -0
- package/src/index.ts +15 -0
- package/src/lib/alert-email.ts +104 -0
- package/src/lib/alerts-store.test.ts +147 -2
- package/src/lib/alerts-store.ts +438 -63
- package/src/lib/metric-alerts.test.ts +769 -0
- package/src/lib/metric-alerts.ts +640 -0
- package/src/lib/metrics-store.ts +9 -2
- package/src/lib/projects-store.test.ts +44 -1
- package/src/lib/projects-store.ts +21 -0
- package/src/lib/sql-db-postgres.integration.test.ts +247 -0
- package/src/lib/store/trace-queries.ts +1 -1
- package/src/lib/webhook-url.test.ts +49 -0
- package/src/lib/webhook-url.ts +72 -14
- package/src/migrations/054_alert_rules_pending_secs.sql +11 -0
- package/src/migrations/055_alert_state_pending_since.sql +12 -0
- package/src/migrations/056_metric_point_series_received.sql +9 -0
- package/src/migrations/057_alert_evaluations_outcome.sql +11 -0
- package/src/migrations/058_alert_state_pending_seen_at.sql +12 -0
- package/src/migrations/059_alert_evaluations_error.sql +11 -0
- package/src/migrations-postgres/054_alert_rules_pending_secs.sql +7 -0
- package/src/migrations-postgres/055_alert_state_pending_since.sql +6 -0
- package/src/migrations-postgres/056_metric_point_series_received.sql +7 -0
- package/src/migrations-postgres/057_alert_evaluations_outcome.sql +6 -0
- package/src/migrations-postgres/058_alert_state_pending_seen_at.sql +6 -0
- package/src/migrations-postgres/059_alert_evaluations_error.sql +6 -0
- package/src/migrations-postgres/README.md +13 -0
- package/src/otlp/decode/logs.ts +30 -2
- package/src/otlp/decode/metrics.ts +67 -2
- package/src/otlp/decode/traces.ts +57 -4
- package/src/otlp/decode/values.ts +64 -10
- package/src/otlp/decode.test.ts +305 -0
- package/src/otlp/decode.ts +9 -3
- package/src/plugins/alerts-evaluator-pending.test.ts +322 -0
- package/src/plugins/alerts-evaluator-retention.test.ts +2 -0
- package/src/plugins/alerts-evaluator.test.ts +10 -2
- package/src/plugins/alerts-evaluator.ts +239 -40
- package/src/plugins/alerts-routes.ts +17 -2
- package/src/plugins/autosre-routes.ts +9 -0
- package/src/plugins/logs-receiver.ts +8 -8
- package/src/plugins/metrics-receiver.ts +10 -9
- package/src/plugins/otlp-receiver.ts +10 -26
- package/src/plugins/profile-routes.test.ts +22 -0
- package/src/plugins/profile-routes.ts +11 -2
- package/src/plugins/replay-query-routes.test.ts +63 -1
- package/src/plugins/replay-query-routes.ts +40 -9
- package/dist/chunk-2FALTDCE.js.map +0 -1
package/README.md
CHANGED
|
@@ -20,10 +20,14 @@ import {
|
|
|
20
20
|
createRetentionCleanupHandler,
|
|
21
21
|
} from "@obsunified/collector";
|
|
22
22
|
|
|
23
|
+
// Ingest keys are validated against the `ingest_keys` table. If the
|
|
24
|
+
// `INGEST_KEY` env binding is set, it is registered as a `bootstrap` key on
|
|
25
|
+
// the first request, so no option is needed for it. Dashboard auth reads
|
|
26
|
+
// `DASHBOARD_PASSWORD` / `DASHBOARD_TOKEN`, and CORS falls back to
|
|
27
|
+
// `ALLOWED_ORIGINS`, from the request env in the same way.
|
|
23
28
|
const app = createDefaultCollectorApp({
|
|
24
|
-
auth: { middleware: createIngestAuth(
|
|
25
|
-
dashboardAuth: createDashboardAuth(
|
|
26
|
-
allowedOrigins: env.ALLOWED_ORIGINS,
|
|
29
|
+
auth: { middleware: createIngestAuth() },
|
|
30
|
+
dashboardAuth: createDashboardAuth(),
|
|
27
31
|
});
|
|
28
32
|
|
|
29
33
|
export default {
|
package/dist/auth.d.ts
CHANGED
package/dist/auth.js
CHANGED
|
@@ -146,11 +146,22 @@ var ProjectsStore = class {
|
|
|
146
146
|
}
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
|
+
const previous = await this.db.prepare(
|
|
150
|
+
`SELECT id, created_at FROM ingest_keys WHERE project_id = 'default' AND name = 'bootstrap' AND revoked_at IS NULL`
|
|
151
|
+
).all();
|
|
149
152
|
const id = randomHex(16);
|
|
150
153
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
151
154
|
await this.db.prepare(
|
|
152
155
|
`INSERT INTO ingest_keys (id, project_id, key_hash, key_prefix, name, created_at) VALUES (?, 'default', ?, ?, 'bootstrap', ?)`
|
|
153
156
|
).bind(id, keyHash, keyPrefix, createdAt).run();
|
|
157
|
+
const stale = previous.results ?? [];
|
|
158
|
+
if (stale.length > 0) {
|
|
159
|
+
console.warn(
|
|
160
|
+
`[projects-store] INGEST_KEY changed: registered a new bootstrap ingest key (${keyPrefix}\u2026) but ${stale.length} earlier bootstrap key(s) are still active and keep accepting ingest: ${stale.map((row) => `id ${row.id} (created ${row.created_at})`).join(
|
|
161
|
+
", "
|
|
162
|
+
)}. Once every client uses the new key, revoke the old one in the dashboard (Projects \u2192 default \u2192 Keys) or with DELETE /internal/projects/default/keys/<keyId>.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
154
165
|
}
|
|
155
166
|
};
|
|
156
167
|
|
|
@@ -336,4 +347,4 @@ export {
|
|
|
336
347
|
resetIngestAuthCache,
|
|
337
348
|
createIngestAuth
|
|
338
349
|
};
|
|
339
|
-
//# sourceMappingURL=chunk-
|
|
350
|
+
//# sourceMappingURL=chunk-VSOZJO6Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/hash.ts","../src/lib/projects-store.ts","../src/lib/sql-db.ts","../src/auth/ingest-auth.ts"],"sourcesContent":["/**\n * Cross-runtime crypto helpers using Web Crypto API.\n * Works in Cloudflare Workers, Node 20+, Deno, Bun.\n */\n\n/** SHA-256 hash of a string, returned as lowercase hex. */\nexport async function sha256Hex(input: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst digest = await crypto.subtle.digest(\"SHA-256\", encoder.encode(input));\n\treturn bytesToHex(new Uint8Array(digest));\n}\n\n/** Generate `bytes` cryptographically random bytes and return as lowercase hex. */\nexport function randomHex(bytes: number): string {\n\tconst buf = new Uint8Array(bytes);\n\tcrypto.getRandomValues(buf);\n\treturn bytesToHex(buf);\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n\tlet out = \"\";\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tout += bytes[i].toString(16).padStart(2, \"0\");\n\t}\n\treturn out;\n}\n","import type {\n\tIngestKey,\n\tIngestKeyRow,\n\tIngestKeyWithPlaintext,\n\tProject,\n\tProjectRow,\n} from \"@obsunified/types\";\nimport { randomHex, sha256Hex } from \"./hash\";\nimport type { SqlDb } from \"./sql-db\";\n\nconst rowToProject = (row: ProjectRow): Project => ({\n\tid: row.id,\n\tname: row.name,\n\tslug: row.slug,\n\tcreatedAt: row.created_at,\n\tpayloadCaptureEnabled:\n\t\trow.payload_capture_enabled === true ||\n\t\trow.payload_capture_enabled === 1 ||\n\t\trow.payload_capture_enabled === \"1\" ||\n\t\trow.payload_capture_enabled === \"true\",\n});\n\nconst rowToKey = (row: IngestKeyRow): IngestKey => ({\n\tid: row.id,\n\tprojectId: row.project_id,\n\tname: row.name,\n\tkeyPrefix: row.key_prefix,\n\tcreatedAt: row.created_at,\n\trevokedAt: row.revoked_at,\n});\n\nconst SLUG_PATTERN = /^[a-z0-9]([a-z0-9-]{0,48}[a-z0-9])?$/;\n\n/**\n * Display identifier for a secret: at most 16 chars (what minted keys show),\n * but never more than a third of the secret, so a short operator-chosen\n * INGEST_KEY can never be returned in full by `keys list`.\n */\nexport function displayPrefix(plaintext: string): string {\n\treturn plaintext.slice(0, Math.min(16, Math.floor(plaintext.length / 3)));\n}\n\nexport class ProjectsStore {\n\tconstructor(private readonly db: SqlDb) {}\n\n\tasync listProjects(): Promise<Project[]> {\n\t\tconst rs = await this.db\n\t\t\t.prepare(`SELECT * FROM projects ORDER BY created_at ASC`)\n\t\t\t.all<ProjectRow>();\n\t\treturn (rs.results ?? []).map(rowToProject);\n\t}\n\n\tasync getProject(id: string): Promise<Project | null> {\n\t\tconst row = await this.db\n\t\t\t.prepare(`SELECT * FROM projects WHERE id = ?`)\n\t\t\t.bind(id)\n\t\t\t.first<ProjectRow>();\n\t\treturn row ? rowToProject(row) : null;\n\t}\n\n\tasync createProject(input: { name: string; slug: string }): Promise<Project> {\n\t\tconst name = input.name.trim();\n\t\tconst slug = input.slug.trim().toLowerCase();\n\t\tif (!name) throw new Error(\"Project name is required\");\n\t\tif (!SLUG_PATTERN.test(slug))\n\t\t\tthrow new Error(\n\t\t\t\t\"Slug must be lowercase letters, digits, and hyphens (max 50 chars)\",\n\t\t\t);\n\n\t\tconst id = randomHex(16);\n\t\tconst createdAt = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT INTO projects (id, name, slug, created_at) VALUES (?, ?, ?, ?)`,\n\t\t\t)\n\t\t\t.bind(id, name, slug, createdAt)\n\t\t\t.run();\n\n\t\treturn { id, name, slug, createdAt, payloadCaptureEnabled: false };\n\t}\n\n\tasync updateProject(\n\t\tid: string,\n\t\tpatch: { name?: string; payloadCaptureEnabled?: boolean },\n\t): Promise<Project | null> {\n\t\tconst existing = await this.getProject(id);\n\t\tif (!existing) return null;\n\n\t\tconst name = patch.name?.trim();\n\t\tlet next = existing;\n\t\tif (name !== undefined && name !== existing.name) {\n\t\t\tawait this.db\n\t\t\t\t.prepare(`UPDATE projects SET name = ? WHERE id = ?`)\n\t\t\t\t.bind(name, id)\n\t\t\t\t.run();\n\t\t\tnext = { ...next, name };\n\t\t}\n\t\tif (\n\t\t\tpatch.payloadCaptureEnabled !== undefined &&\n\t\t\tpatch.payloadCaptureEnabled !== existing.payloadCaptureEnabled\n\t\t) {\n\t\t\tawait this.db\n\t\t\t\t.prepare(`UPDATE projects SET payload_capture_enabled = ? WHERE id = ?`)\n\t\t\t\t.bind(patch.payloadCaptureEnabled ? 1 : 0, id)\n\t\t\t\t.run();\n\t\t\tnext = {\n\t\t\t\t...next,\n\t\t\t\tpayloadCaptureEnabled: patch.payloadCaptureEnabled,\n\t\t\t};\n\t\t}\n\t\treturn next;\n\t}\n\n\tasync ensureDefaultProject(): Promise<void> {\n\t\tawait this.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT OR IGNORE INTO projects (id, name, slug, created_at) VALUES ('default', 'Default', 'default', ?)`,\n\t\t\t)\n\t\t\t.bind(new Date().toISOString())\n\t\t\t.run();\n\t}\n\n\t// ── Ingest keys ──\n\n\tasync listKeys(projectId: string): Promise<IngestKey[]> {\n\t\tconst rs = await this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT * FROM ingest_keys WHERE project_id = ? ORDER BY created_at DESC`,\n\t\t\t)\n\t\t\t.bind(projectId)\n\t\t\t.all<IngestKeyRow>();\n\t\treturn (rs.results ?? []).map(rowToKey);\n\t}\n\n\tasync createKey(\n\t\tprojectId: string,\n\t\tname: string,\n\t): Promise<IngestKeyWithPlaintext> {\n\t\tconst project = await this.getProject(projectId);\n\t\tif (!project) throw new Error(\"Project not found\");\n\n\t\tconst trimmedName = name.trim() || \"unnamed\";\n\t\tconst id = randomHex(16);\n\t\tconst plaintext = `obs_${project.slug}_${randomHex(24)}`;\n\t\tconst keyHash = await sha256Hex(plaintext);\n\t\t// Prefix: show enough to identify but not recover (first 16 chars).\n\t\tconst keyPrefix = plaintext.slice(0, 16);\n\t\tconst createdAt = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT INTO ingest_keys (id, project_id, key_hash, key_prefix, name, created_at) VALUES (?, ?, ?, ?, ?, ?)`,\n\t\t\t)\n\t\t\t.bind(id, projectId, keyHash, keyPrefix, trimmedName, createdAt)\n\t\t\t.run();\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprojectId,\n\t\t\tname: trimmedName,\n\t\t\tkeyPrefix,\n\t\t\tcreatedAt,\n\t\t\trevokedAt: null,\n\t\t\tkey: plaintext,\n\t\t\twarning: \"This key will not be shown again. Store it somewhere safe.\",\n\t\t};\n\t}\n\n\tasync revokeKey(keyId: string, projectId: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.prepare(\n\t\t\t\t`UPDATE ingest_keys SET revoked_at = ?\n\t\t\t\t WHERE id = ? AND project_id = ? AND revoked_at IS NULL`,\n\t\t\t)\n\t\t\t.bind(new Date().toISOString(), keyId, projectId)\n\t\t\t.run();\n\t\treturn (result.meta?.changes ?? 0) > 0;\n\t}\n\n\tasync findByKeyHash(\n\t\tkeyHash: string,\n\t): Promise<{ projectId: string; keyId: string } | null> {\n\t\tconst row = await this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT id, project_id FROM ingest_keys WHERE key_hash = ? AND revoked_at IS NULL`,\n\t\t\t)\n\t\t\t.bind(keyHash)\n\t\t\t.first<{ id: string; project_id: string }>();\n\t\treturn row ? { projectId: row.project_id, keyId: row.id } : null;\n\t}\n\n\t/**\n\t * Idempotently register the legacy env INGEST_KEY as a bootstrap key on the\n\t * default project, so existing deployments keep working without manual migration.\n\t */\n\tasync bootstrapEnvKey(plaintext: string): Promise<void> {\n\t\tif (!plaintext) return;\n\t\tawait this.ensureDefaultProject();\n\t\tconst keyHash = await sha256Hex(plaintext);\n\t\tconst keyPrefix = displayPrefix(plaintext);\n\t\tconst existing = await this.db\n\t\t\t.prepare(`SELECT id, key_prefix FROM ingest_keys WHERE key_hash = ?`)\n\t\t\t.bind(keyHash)\n\t\t\t.first<{ id: string; key_prefix: string }>();\n\t\tif (existing) {\n\t\t\t// Rows written before the prefix was length-capped may hold a short\n\t\t\t// bootstrap key in full; shrink them to the safe display prefix.\n\t\t\tif (existing.key_prefix.length > keyPrefix.length) {\n\t\t\t\tawait this.db\n\t\t\t\t\t.prepare(`UPDATE ingest_keys SET key_prefix = ? WHERE id = ?`)\n\t\t\t\t\t.bind(keyPrefix, existing.id)\n\t\t\t\t\t.run();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// A different, still-active bootstrap key means INGEST_KEY was rotated.\n\t\t// The old key is deliberately NOT revoked here: other instances (or\n\t\t// clients mid-rollout) may still be using it. Tell the operator instead.\n\t\tconst previous = await this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT id, created_at FROM ingest_keys WHERE project_id = 'default' AND name = 'bootstrap' AND revoked_at IS NULL`,\n\t\t\t)\n\t\t\t.all<{ id: string; created_at: string }>();\n\n\t\tconst id = randomHex(16);\n\t\tconst createdAt = new Date().toISOString();\n\t\tawait this.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT INTO ingest_keys (id, project_id, key_hash, key_prefix, name, created_at) VALUES (?, 'default', ?, ?, 'bootstrap', ?)`,\n\t\t\t)\n\t\t\t.bind(id, keyHash, keyPrefix, createdAt)\n\t\t\t.run();\n\n\t\tconst stale = previous.results ?? [];\n\t\tif (stale.length > 0) {\n\t\t\tconsole.warn(\n\t\t\t\t`[projects-store] INGEST_KEY changed: registered a new bootstrap ingest key (${keyPrefix}…) but ${stale.length} earlier bootstrap key(s) are still active and keep accepting ingest: ${stale\n\t\t\t\t\t// Ids only: legacy rows may still hold a long key_prefix.\n\t\t\t\t\t.map((row) => `id ${row.id} (created ${row.created_at})`)\n\t\t\t\t\t.join(\n\t\t\t\t\t\t\", \",\n\t\t\t\t\t)}. Once every client uses the new key, revoke the old one in the dashboard (Projects → default → Keys) or with DELETE /internal/projects/default/keys/<keyId>.`,\n\t\t\t);\n\t\t}\n\t}\n}\n","/**\n * Storage seam for the collector — RFC 0008.\n *\n * Stores and plugins target this interface instead of `D1Database` directly,\n * so a future Node / better-sqlite3 / ClickHouse runtime is one new adapter\n * away rather than a fork-and-rewrite. The shape is deliberately D1's\n * existing `prepare → bind → first|all|run` chain so the migration of\n * existing stores is mechanical (search-and-replace the constructor type).\n *\n * Today there is one implementation, `D1Adapter`. A `MemSqlDb` test double\n * lives next door under `test-utils/`; `BetterSqliteAdapter` and\n * `ClickHouseAdapter` are deferred until they have a real runtime to serve.\n */\n\nexport interface SqlDb {\n\treadonly dialect?: SqlDialect;\n\tprepare(sql: string): SqlStatement;\n\t/**\n\t * Run multiple prepared statements as a batch. On D1 this maps to a\n\t * single network round-trip; on a future better-sqlite3 adapter it\n\t * wraps the calls in a transaction. Returns one result per statement\n\t * in input order, mirroring D1's `batch`.\n\t *\n\t * Statements MUST have been produced by this same `SqlDb` instance —\n\t * mixing adapters is an error and runtime behavior is undefined.\n\t */\n\tbatch(\n\t\tstatements: SqlStatement[],\n\t): Promise<Array<{ meta: { changes: number } }>>;\n}\n\nexport type SqlDialectName = \"sqlite\" | \"postgres\";\n\nexport interface SqlDialect {\n\treadonly name: SqlDialectName;\n\tnow(): string;\n\tsinceHours(param: string): string;\n\tsinceMinutes(param: string): string;\n\tjsonText(expr: string, path: string): string;\n}\n\nexport interface SqlStatement {\n\tbind(...args: unknown[]): SqlStatement;\n\tfirst<T = Record<string, unknown>>(): Promise<T | null>;\n\tall<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n\trun(): Promise<{ meta: { changes: number } }>;\n}\n\n/**\n * D1 already conforms to this shape almost exactly; the adapter normalizes\n * two small differences:\n * - `all()` on D1 returns `results?: T[]` (optional). We coerce to `[]`.\n * - `run()`'s `meta.changes` is optional on D1. We coerce to `0`.\n * Both differences are runtime-undetectable for the queries this codebase\n * issues (every successful all() returns results; every run() against an\n * INSERT/UPDATE/DELETE reports changes), but coercing keeps the typed\n * interface honest.\n */\nexport class D1Adapter implements SqlDb {\n\treadonly dialect = sqliteDialect;\n\n\tconstructor(private readonly d1: D1Database) {}\n\n\tprepare(sql: string): SqlStatement {\n\t\treturn new D1StatementAdapter(this.d1.prepare(sql));\n\t}\n\n\tasync batch(\n\t\tstatements: SqlStatement[],\n\t): Promise<Array<{ meta: { changes: number } }>> {\n\t\t// Unwrap each statement back to its D1 form — D1's batch takes\n\t\t// D1PreparedStatement[], not our typed wrapper.\n\t\tconst unwrapped = statements.map((s) => {\n\t\t\tif (!(s instanceof D1StatementAdapter)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"D1Adapter.batch: statement was not produced by D1Adapter\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn s.unwrap();\n\t\t});\n\t\tconst results = await this.d1.batch(unwrapped);\n\t\treturn results.map((r) => ({\n\t\t\tmeta: { changes: r.meta?.changes ?? 0 },\n\t\t}));\n\t}\n}\n\n/**\n * Convenience wrapper for the common case: take a `CollectorEnv`-shaped\n * object and return an `SqlDb`. Most plugins don't have access to the\n * `CollectorRuntime` (they get it at register time, not handler time)\n * so they call this directly. Plugins that *do* hold the runtime should\n * prefer `runtime.getSqlDb(env)` so a host-supplied `sqlDb` factory in\n * `CollectorConfig` takes effect.\n */\nexport const sqlDbFor = (env: { DB: D1Database | SqlDb }): SqlDb =>\n\tisSqlDb(env.DB) ? env.DB : new D1Adapter(env.DB);\n\n/**\n * SQLite `strftime` format that reproduces `Date#toISOString()` output\n * (`YYYY-MM-DDTHH:MM:SS.mmmZ`). Every stored timestamp column holds that\n * shape, so time windows must be rendered the same way: `datetime('now')`\n * yields `YYYY-MM-DD HH:MM:SS` and in a text comparison the space sorts\n * before the `T`, which made \"last 5 minutes\" match the whole current UTC\n * day on D1 and made `expires_at < now` purges run a day late.\n */\nexport const SQLITE_ISO_FORMAT = \"%Y-%m-%dT%H:%M:%fZ\";\n\n/** Render SQLite `now` (optionally shifted by a modifier expression) as ISO text. */\nexport const sqliteIsoNow = (modifier?: string): string =>\n\tmodifier\n\t\t? `strftime('${SQLITE_ISO_FORMAT}', 'now', ${modifier})`\n\t\t: `strftime('${SQLITE_ISO_FORMAT}', 'now')`;\n\nexport const sqliteDialect: SqlDialect = {\n\tname: \"sqlite\",\n\tnow: () => sqliteIsoNow(),\n\tsinceHours: (param) => sqliteIsoNow(`'-' || ${param} || ' hours'`),\n\tsinceMinutes: (param) => sqliteIsoNow(`'-' || ${param} || ' minutes'`),\n\tjsonText: (expr, path) => `json_extract(${expr}, '${path}')`,\n};\n\n/**\n * The collector stores every timestamp column (`received_at`, `expires_at`,\n * `occurred_at`, …) as TEXT holding `Date#toISOString()` output on both\n * D1 and Postgres. Postgres will not compare TEXT against a\n * `timestamptz`, so time-window expressions have to be rendered as ISO\n * text in the same `YYYY-MM-DDTHH:MM:SS.mmmZ` shape the collector writes.\n */\nconst POSTGRES_ISO_TEXT_FORMAT = `'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'`;\nconst POSTGRES_UTC_NOW = \"(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')\";\n\n/** Render a Postgres `timestamp` expression as collector-style ISO text. */\nexport const postgresIsoText = (expr: string): string =>\n\t`to_char(${expr}, ${POSTGRES_ISO_TEXT_FORMAT})`;\n\nexport const postgresDialect: SqlDialect = {\n\tname: \"postgres\",\n\tnow: () => postgresIsoText(POSTGRES_UTC_NOW),\n\tsinceHours: (param) =>\n\t\tpostgresIsoText(\n\t\t\t`${POSTGRES_UTC_NOW} - (${param}::text || ' hours')::interval`,\n\t\t),\n\tsinceMinutes: (param) =>\n\t\tpostgresIsoText(\n\t\t\t`${POSTGRES_UTC_NOW} - (${param}::text || ' minutes')::interval`,\n\t\t),\n\tjsonText: (expr, path) => {\n\t\tconst key = path\n\t\t\t.replace(/^\\$\\./, \"\")\n\t\t\t.replace(/^\"|\"$/g, \"\")\n\t\t\t.replace(/\\\\u002E/g, \".\");\n\t\treturn `(${expr}::jsonb ->> '${key.replace(/'/g, \"''\")}')`;\n\t},\n};\n\nexport const dialectFor = (db: SqlDb): SqlDialect =>\n\tdb.dialect ?? sqliteDialect;\n\nconst isSqlDb = (db: unknown): db is SqlDb => {\n\tif (!db || typeof db !== \"object\") return false;\n\tconst candidate = db as {\n\t\tprepare?: unknown;\n\t\tbatch?: unknown;\n\t\texec?: unknown;\n\t};\n\treturn (\n\t\ttypeof candidate.prepare === \"function\" &&\n\t\ttypeof candidate.batch === \"function\" &&\n\t\ttypeof candidate.exec !== \"function\"\n\t);\n};\n\nclass D1StatementAdapter implements SqlStatement {\n\tconstructor(private readonly stmt: D1PreparedStatement) {}\n\n\t/** Internal escape hatch for `D1Adapter.batch` to recover the wrapped\n\t * D1 statement. Not part of the public `SqlStatement` shape. */\n\tunwrap(): D1PreparedStatement {\n\t\treturn this.stmt;\n\t}\n\n\tbind(...args: unknown[]): SqlStatement {\n\t\treturn new D1StatementAdapter(this.stmt.bind(...args));\n\t}\n\n\tfirst<T = Record<string, unknown>>(): Promise<T | null> {\n\t\treturn this.stmt.first<T>();\n\t}\n\n\tasync all<T = Record<string, unknown>>(): Promise<{ results: T[] }> {\n\t\tconst r = await this.stmt.all<T>();\n\t\treturn { results: r.results ?? [] };\n\t}\n\n\tasync run(): Promise<{ meta: { changes: number } }> {\n\t\tconst r = await this.stmt.run();\n\t\treturn { meta: { changes: r.meta?.changes ?? 0 } };\n\t}\n}\n","import type { MiddlewareHandler } from \"hono\";\nimport type { CollectorEnv } from \"../framework/env\";\nimport { sha256Hex } from \"../lib/hash\";\nimport { ProjectsStore } from \"../lib/projects-store\";\nimport { sqlDbFor } from \"../lib/sql-db\";\n\nconst CACHE_TTL_MS = 60_000; // 60 seconds\nconst CACHE_MAX_ENTRIES = 500;\n\ninterface CacheEntry {\n\tprojectId: string;\n\texpiresAt: number;\n}\n\nlet keyCaches = new WeakMap<object, Map<string, CacheEntry>>();\nlet bootstrapDone = false;\n\nfunction cacheFor(dbKey: object): Map<string, CacheEntry> {\n\tlet cache = keyCaches.get(dbKey);\n\tif (!cache) {\n\t\tcache = new Map();\n\t\tkeyCaches.set(dbKey, cache);\n\t}\n\treturn cache;\n}\n\nfunction getCached(\n\tcache: Map<string, CacheEntry>,\n\thash: string,\n\tnow: number,\n): string | null {\n\tpruneKeyCache(cache, now);\n\tconst entry = cache.get(hash);\n\tif (!entry) return null;\n\tif (entry.expiresAt <= now) {\n\t\tcache.delete(hash);\n\t\treturn null;\n\t}\n\treturn entry.projectId;\n}\n\nfunction setCached(\n\tcache: Map<string, CacheEntry>,\n\thash: string,\n\tprojectId: string,\n\tnow: number,\n): void {\n\tpruneKeyCache(cache, now);\n\twhile (cache.size >= CACHE_MAX_ENTRIES) {\n\t\tconst oldestKey = cache.keys().next().value;\n\t\tif (!oldestKey) break;\n\t\tcache.delete(oldestKey);\n\t}\n\tcache.set(hash, { projectId, expiresAt: now + CACHE_TTL_MS });\n}\n\nfunction pruneKeyCache(cache: Map<string, CacheEntry>, now: number): void {\n\tfor (const [hash, entry] of cache) {\n\t\tif (entry.expiresAt <= now) cache.delete(hash);\n\t}\n}\n\n/** Test helper — clears the in-memory cache and bootstrap flag. */\nexport function resetIngestAuthCache(): void {\n\tkeyCaches = new WeakMap();\n\tbootstrapDone = false;\n}\n\n/**\n * Creates middleware that validates ingest API keys against the ingest_keys table.\n *\n * Accepts the key from either:\n * - Authorization: Bearer <key>\n * - X-API-Key: <key>\n *\n * On success, sets `projectId` on the Hono context via `c.set('projectId', ...)`.\n *\n * On the first request per isolate, if `env.INGEST_KEY` is set, the middleware\n * registers it as a `bootstrap` key on the default project — so existing\n * single-tenant deployments continue to work without manual migration.\n */\nexport function createIngestAuth(config?: {\n\tallowUnauthenticated?: boolean;\n}): MiddlewareHandler<{\n\tBindings: CollectorEnv;\n\tVariables: { projectId: string };\n}> {\n\treturn async (c, next) => {\n\t\t// Lazy bootstrap of legacy env key (idempotent).\n\t\tif (!bootstrapDone) {\n\t\t\tconst legacy = c.env.INGEST_KEY || c.env.TELEMETRY_INGEST_TOKEN;\n\t\t\tif (legacy) {\n\t\t\t\tconst store = new ProjectsStore(sqlDbFor(c.env));\n\t\t\t\ttry {\n\t\t\t\t\tawait store.bootstrapEnvKey(legacy);\n\t\t\t\t\t// Latch only after success: a transient DB error on the first\n\t\t\t\t\t// request must not permanently disable legacy-key bootstrap for\n\t\t\t\t\t// the isolate. bootstrapEnvKey is idempotent, so concurrent\n\t\t\t\t\t// first-requests racing here are harmless.\n\t\t\t\t\tbootstrapDone = true;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"[ingest-auth] bootstrap failed:\", err);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Nothing to bootstrap — don't keep re-checking on every request.\n\t\t\t\tbootstrapDone = true;\n\t\t\t}\n\t\t}\n\n\t\tconst bearer = c.req.header(\"Authorization\")?.replace(/^Bearer\\s+/i, \"\");\n\t\tconst apiKey = c.req.header(\"X-API-Key\");\n\t\tconst token = bearer || apiKey;\n\n\t\tif (!token) {\n\t\t\tif (\n\t\t\t\tconfig?.allowUnauthenticated ||\n\t\t\t\tc.env.ALLOW_UNAUTHENTICATED === \"true\"\n\t\t\t) {\n\t\t\t\tc.set(\"projectId\", \"default\");\n\t\t\t\treturn next();\n\t\t\t}\n\t\t\treturn c.json({ error: \"Unauthorized\" }, 401);\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst hash = await sha256Hex(token);\n\t\tconst rawDb =\n\t\t\t(c.env.DB as unknown as Record<string, unknown>)?.__unwrapped_db ||\n\t\t\tc.env.DB;\n\t\tconst cache = cacheFor(rawDb);\n\t\tconst cached = getCached(cache, hash, now);\n\t\tif (cached) {\n\t\t\tc.set(\"projectId\", cached);\n\t\t\treturn next();\n\t\t}\n\n\t\tconst store = new ProjectsStore(sqlDbFor(c.env));\n\t\tconst match = await store.findByKeyHash(hash);\n\t\tif (!match) {\n\t\t\treturn c.json({ error: \"Unauthorized\" }, 401);\n\t\t}\n\n\t\tsetCached(cache, hash, match.projectId, now);\n\t\tc.set(\"projectId\", match.projectId);\n\t\treturn next();\n\t};\n}\n"],"mappings":"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","names":["store"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import * as hono from 'hono';
|
|
|
2
2
|
import { MiddlewareHandler, Hono, Context } from 'hono';
|
|
3
3
|
import * as hono_types from 'hono/types';
|
|
4
4
|
import * as _obsunified_types from '@obsunified/types';
|
|
5
|
-
import { StoredSpan, TelemetryOverviewOptions, TelemetryOverviewResponse, TelemetryTraceDetailResponse, TelemetryInstrumentationGapsResponse, TelemetryIssueOptions, TelemetryIssueOverviewResponse, TelemetryIssueDetailResponse, UsageEventRecord, UsageOverviewOptions, UsageOverviewResponse, UsageSessionDetailResponse, UsageSessionSummary, AlertRuleInput, AlertRule, AlertStateRow, AlertState, AlertEvaluationsListResponse, AlertComparison, ChangeBlastRadiusResult, ContentionCandidate, ChangeEvent, ChangeEventInput, InvestigationFinding, InvestigationFindingInput, OntologyNode, OntologyEdge, CanonicalNodeIdentity, OntologyGraphResponse, RelationshipCertainty, CausalTriple, IncidentInvestigation, RemediationProposal, EvidenceEntityKind, RemediationProposalInput, JsonValue, EvidenceReference, EvidenceReferenceContract, Project, IngestKey, IngestKeyWithPlaintext } from '@obsunified/types';
|
|
6
|
-
import {
|
|
7
|
-
export { C as COLLECTOR_HOST_MANAGED_ENV_KEYS,
|
|
5
|
+
import { StoredSpan, TelemetryOverviewOptions, TelemetryOverviewResponse, TelemetryTraceDetailResponse, TelemetryInstrumentationGapsResponse, TelemetryIssueOptions, TelemetryIssueOverviewResponse, TelemetryIssueDetailResponse, UsageEventRecord, UsageOverviewOptions, UsageOverviewResponse, UsageSessionDetailResponse, UsageSessionSummary, AlertRuleInput, AlertRule, AlertStateRow, AlertState, AlertEvaluationOutcome, AlertEvaluationsListResponse, AlertComparison, ChangeBlastRadiusResult, ContentionCandidate, ChangeEvent, ChangeEventInput, InvestigationFinding, InvestigationFindingInput, OntologyNode, OntologyEdge, CanonicalNodeIdentity, OntologyGraphResponse, RelationshipCertainty, CausalTriple, IncidentInvestigation, RemediationProposal, EvidenceEntityKind, RemediationProposalInput, JsonValue, EvidenceReference, EvidenceReferenceContract, AlertQueryMetrics, AlertMetricAggregation, AlertMetricField, Project, IngestKey, IngestKeyWithPlaintext } from '@obsunified/types';
|
|
6
|
+
import { g as CollectorEnv, L as Logger, e as ChildSpanRunner, l as CollectorRouteContext, a as AlertEmailTransport } from './ingest-auth-BTowieER.js';
|
|
7
|
+
export { A as AlertEmailMessage, b as AlertNotificationPayload, C as COLLECTOR_HOST_MANAGED_ENV_KEYS, c as COLLECTOR_OPTIONAL_STRING_ENV_KEYS, d as COLLECTOR_STRING_ENV_MANIFEST, f as CollectorApp, h as CollectorHostManagedEnvKey, i as CollectorManifestStringEnvKey, j as CollectorObjectBindingKey, k as CollectorOptionalStringEnvKey, m as CollectorStringEnvKey, n as assertCollectorEnvManifestCompleteness, o as consoleLogger, p as createIngestAuth, q as formatAlertEmail, r as isValidEmailAddress } from './ingest-auth-BTowieER.js';
|
|
8
8
|
import { ActionConfidence } from '@obsunified/types/constants';
|
|
9
9
|
import { Pool } from 'pg';
|
|
10
10
|
|
|
@@ -389,6 +389,13 @@ interface CollectorConfig {
|
|
|
389
389
|
* client can never make the host buffer an ingest-sized body.
|
|
390
390
|
*/
|
|
391
391
|
maxAuthBodyBytes?: number;
|
|
392
|
+
/**
|
|
393
|
+
* Acknowledge that this app runs without `auth` on `/v1/*` (e.g. a local
|
|
394
|
+
* sandbox behind a trusted proxy). It changes nothing about request
|
|
395
|
+
* handling; it only silences the one-time warning logged when the first
|
|
396
|
+
* ingest request reaches an app that has no ingest auth configured.
|
|
397
|
+
*/
|
|
398
|
+
allowUnauthenticatedIngest?: boolean;
|
|
392
399
|
}
|
|
393
400
|
declare class CollectorRuntime implements CollectorPluginContext {
|
|
394
401
|
private readonly spanProcessors;
|
|
@@ -488,15 +495,178 @@ declare const aiReceiverPlugin: CollectorPlugin;
|
|
|
488
495
|
|
|
489
496
|
declare const aiSpanPayloadsProcessorPlugin: CollectorPlugin;
|
|
490
497
|
|
|
498
|
+
/** A rule's value, and whether a metrics rule found no fresh data. */
|
|
499
|
+
interface AlertRuleEvaluation {
|
|
500
|
+
value: number;
|
|
501
|
+
noData: boolean;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
interface AlertsStoreOptions {
|
|
505
|
+
/** Permit webhook targets on loopback / private / link-local hosts. */
|
|
506
|
+
allowPrivateWebhookTargets?: boolean;
|
|
507
|
+
/**
|
|
508
|
+
* Whether this collector can send email (a transport is bound as
|
|
509
|
+
* `env.ALERT_EMAIL`). Rules with an email channel are refused otherwise.
|
|
510
|
+
*/
|
|
511
|
+
emailDeliveryAvailable?: boolean;
|
|
512
|
+
}
|
|
513
|
+
declare function compareValue(value: number, threshold: number, comparison: AlertComparison): boolean;
|
|
514
|
+
declare class AlertsStore {
|
|
515
|
+
private readonly db;
|
|
516
|
+
private readonly options;
|
|
517
|
+
constructor(db: SqlDb, options?: AlertsStoreOptions);
|
|
518
|
+
validateInput(input: AlertRuleInput): void;
|
|
519
|
+
listRules(projectId: string): Promise<AlertRule[]>;
|
|
520
|
+
/**
|
|
521
|
+
* The newest evaluation of every rule in the project, one query. Falls
|
|
522
|
+
* back to the pre-057/059 column set on older databases.
|
|
523
|
+
*/
|
|
524
|
+
private lastEvaluations;
|
|
525
|
+
getRule(id: string, projectId: string): Promise<AlertRule | null>;
|
|
526
|
+
createRule(projectId: string, input: AlertRuleInput): Promise<AlertRule>;
|
|
527
|
+
updateRule(id: string, projectId: string, patch: Partial<AlertRuleInput>): Promise<AlertRule | null>;
|
|
528
|
+
/**
|
|
529
|
+
* Clears a rule's pending period. Tolerates databases that have not
|
|
530
|
+
* applied 055/058 (there is nothing to clear on them).
|
|
531
|
+
*/
|
|
532
|
+
resetPending(ruleId: string, projectId: string): Promise<void>;
|
|
533
|
+
/**
|
|
534
|
+
* Runs `current`; on a database that has not applied migration 054 (no
|
|
535
|
+
* `alert_rules.pending_secs`) a rule without a pending duration is
|
|
536
|
+
* written with the legacy statement so rolling deploys keep working. A
|
|
537
|
+
* rule that needs the column is refused with a clear message instead of
|
|
538
|
+
* silently losing its pending duration.
|
|
539
|
+
*/
|
|
540
|
+
private withPendingColumnFallback;
|
|
541
|
+
deleteRule(id: string, projectId: string): Promise<boolean>;
|
|
542
|
+
listEnabledRules(): Promise<AlertRule[]>;
|
|
543
|
+
getState(ruleId: string, projectId: string): Promise<AlertStateRow | null>;
|
|
544
|
+
/**
|
|
545
|
+
* Record a state transition with its notification still outstanding
|
|
546
|
+
* (`notified = 0`); `markNotified` flips it once a channel accepts the
|
|
547
|
+
* alert. On a database that has not applied migration 049 the column is
|
|
548
|
+
* missing: the transition is still written (legacy statement) so alerting
|
|
549
|
+
* keeps working through a rolling deploy, and the evaluator treats a row
|
|
550
|
+
* without the column as delivered.
|
|
551
|
+
*/
|
|
552
|
+
transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
|
|
553
|
+
/**
|
|
554
|
+
* Mark the transition `(state, lastStateChange)` as delivered. Scoped to
|
|
555
|
+
* the exact transition the caller notified for: if the rule changed state
|
|
556
|
+
* again while the webhook was in flight, the newer transition keeps its
|
|
557
|
+
* own outstanding flag. Returns false when nothing matched (stale or
|
|
558
|
+
* already marked). On a pre-049 schema there is nothing to mark.
|
|
559
|
+
*/
|
|
560
|
+
markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
|
|
561
|
+
/**
|
|
562
|
+
* Record (`{ since, seenAt }`) or clear (`null`) the pending period of a
|
|
563
|
+
* rule that has a pending duration (migrations 055, 058): when the
|
|
564
|
+
* condition started holding, and the last evaluation that saw it hold.
|
|
565
|
+
* A rule without a state row gets one in state `ok` since its creation,
|
|
566
|
+
* already delivered (nothing is owed for a rule that never changed
|
|
567
|
+
* state).
|
|
568
|
+
*/
|
|
569
|
+
setPending(ruleId: string, projectId: string, pending: {
|
|
570
|
+
since: string;
|
|
571
|
+
seenAt: string;
|
|
572
|
+
} | null, ruleCreatedAt: string): Promise<void>;
|
|
573
|
+
recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean, outcome?: AlertEvaluationOutcome, errorMessage?: string): Promise<void>;
|
|
574
|
+
listEvaluations(params: {
|
|
575
|
+
ruleId: string;
|
|
576
|
+
projectId?: string;
|
|
577
|
+
hours: number;
|
|
578
|
+
limit?: number;
|
|
579
|
+
}): Promise<AlertEvaluationsListResponse>;
|
|
580
|
+
evaluateRule(rule: AlertRule, now?: Date): Promise<number>;
|
|
581
|
+
/** Like `evaluateRule`, plus whether a metrics rule found no fresh data. */
|
|
582
|
+
evaluateRuleWithStatus(rule: AlertRule, now?: Date): Promise<AlertRuleEvaluation>;
|
|
583
|
+
private evaluateCountRule;
|
|
584
|
+
/**
|
|
585
|
+
* Stage 6 — fetch the latest narrative associated with this rule's
|
|
586
|
+
* bound analysis. Used by the evaluator when firing an analysis-bound
|
|
587
|
+
* alert so the webhook payload includes the human-readable
|
|
588
|
+
* narrative, not just a threshold crossing.
|
|
589
|
+
*/
|
|
590
|
+
getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
|
|
591
|
+
narrative: string | null;
|
|
592
|
+
status: string | null;
|
|
593
|
+
} | null>;
|
|
594
|
+
private evaluateSpanRule;
|
|
595
|
+
private evaluateLogRule;
|
|
596
|
+
private evaluateUsageRule;
|
|
597
|
+
private evaluateAIRule;
|
|
598
|
+
}
|
|
599
|
+
|
|
491
600
|
/** Resolve a hostname to its addresses (e.g. `dns.lookup(host, { all: true })`). */
|
|
492
601
|
type HostLookup = (hostname: string) => Promise<string[]>;
|
|
493
602
|
|
|
603
|
+
interface AlertEvaluatorStore {
|
|
604
|
+
listEnabledRules(): Promise<AlertRule[]>;
|
|
605
|
+
evaluateRule(rule: AlertRule): Promise<number>;
|
|
606
|
+
/** Preferred when present: also says whether a metrics rule had no data. */
|
|
607
|
+
evaluateRuleWithStatus?(rule: AlertRule): Promise<AlertRuleEvaluation>;
|
|
608
|
+
getState(ruleId: string, projectId: string): Promise<{
|
|
609
|
+
current_state: AlertState;
|
|
610
|
+
last_state_change: string;
|
|
611
|
+
/** 0 while the transition's notification is owed; absent pre-049 (= delivered). */
|
|
612
|
+
notified?: number;
|
|
613
|
+
/** Start of the current pending period (migration 055); null/absent when not pending. */
|
|
614
|
+
pending_since?: string | null;
|
|
615
|
+
/** Last evaluation that saw the pending condition hold (migration 058). */
|
|
616
|
+
pending_seen_at?: string | null;
|
|
617
|
+
} | null>;
|
|
618
|
+
/** Record (`{ since, seenAt }`) or clear (null) the rule's pending period. */
|
|
619
|
+
setPending(ruleId: string, projectId: string, pending: {
|
|
620
|
+
since: string;
|
|
621
|
+
seenAt: string;
|
|
622
|
+
} | null, ruleCreatedAt: string): Promise<void>;
|
|
623
|
+
/** Flip the exact transition `(state, lastStateChange)` to delivered. */
|
|
624
|
+
markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
|
|
625
|
+
getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
|
|
626
|
+
narrative: string | null;
|
|
627
|
+
status: string | null;
|
|
628
|
+
} | null>;
|
|
629
|
+
transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
|
|
630
|
+
recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean, outcome?: AlertEvaluationOutcome,
|
|
631
|
+
/** With outcome `error`: why (the store sanitizes it). */
|
|
632
|
+
errorMessage?: string): Promise<void>;
|
|
633
|
+
}
|
|
634
|
+
declare function evaluateRuleBatch(store: AlertEvaluatorStore, rules: AlertRule[], options?: {
|
|
635
|
+
logger?: Logger;
|
|
636
|
+
tracer?: ChildSpanRunner;
|
|
637
|
+
concurrency?: number;
|
|
638
|
+
ruleTimeoutMs?: number;
|
|
639
|
+
allowPrivateWebhookTargets?: boolean;
|
|
640
|
+
hostLookup?: HostLookup;
|
|
641
|
+
email?: AlertEmailTransport;
|
|
642
|
+
onAlertFiring?: (rule: AlertRule) => Promise<void>;
|
|
643
|
+
/**
|
|
644
|
+
* Longest gap between two evaluations that still continues a pending
|
|
645
|
+
* hold. Default `DEFAULT_MAX_EVALUATION_GAP_MS` (two 5-minute ticks);
|
|
646
|
+
* pass twice the host's evaluation interval.
|
|
647
|
+
*/
|
|
648
|
+
maxEvaluationGapMs?: number;
|
|
649
|
+
/**
|
|
650
|
+
* ISO start of this collector process. A hold last seen before it
|
|
651
|
+
* starts over, as Prometheus resets `for` on restart. Unset on hosts
|
|
652
|
+
* without a long-lived process (Workers), where the gap rule applies.
|
|
653
|
+
*/
|
|
654
|
+
holdStartedAt?: string;
|
|
655
|
+
}): Promise<{
|
|
656
|
+
evaluated: number;
|
|
657
|
+
fired: number;
|
|
658
|
+
resolved: number;
|
|
659
|
+
/** Notifications for an earlier transition that were delivered on this tick. */
|
|
660
|
+
redelivered: number;
|
|
661
|
+
}>;
|
|
494
662
|
declare function evaluateAllRules(env: CollectorEnv, options?: {
|
|
495
663
|
logger?: Logger;
|
|
496
664
|
tracer?: ChildSpanRunner;
|
|
497
665
|
concurrency?: number;
|
|
498
666
|
ruleTimeoutMs?: number;
|
|
499
667
|
hostLookup?: HostLookup;
|
|
668
|
+
maxEvaluationGapMs?: number;
|
|
669
|
+
holdStartedAt?: string;
|
|
500
670
|
}): Promise<{
|
|
501
671
|
evaluated: number;
|
|
502
672
|
fired: number;
|
|
@@ -1024,64 +1194,6 @@ declare class ActionStore {
|
|
|
1024
1194
|
listArtifactsByAction(projectId: string, actionId: string, limit?: number): Promise<ArtifactRef[]>;
|
|
1025
1195
|
}
|
|
1026
1196
|
|
|
1027
|
-
interface AlertsStoreOptions {
|
|
1028
|
-
/** Permit webhook targets on loopback / private / link-local hosts. */
|
|
1029
|
-
allowPrivateWebhookTargets?: boolean;
|
|
1030
|
-
}
|
|
1031
|
-
declare function compareValue(value: number, threshold: number, comparison: AlertComparison): boolean;
|
|
1032
|
-
declare class AlertsStore {
|
|
1033
|
-
private readonly db;
|
|
1034
|
-
private readonly options;
|
|
1035
|
-
constructor(db: SqlDb, options?: AlertsStoreOptions);
|
|
1036
|
-
validateInput(input: AlertRuleInput): void;
|
|
1037
|
-
listRules(projectId: string): Promise<AlertRule[]>;
|
|
1038
|
-
getRule(id: string, projectId: string): Promise<AlertRule | null>;
|
|
1039
|
-
createRule(projectId: string, input: AlertRuleInput): Promise<AlertRule>;
|
|
1040
|
-
updateRule(id: string, projectId: string, patch: Partial<AlertRuleInput>): Promise<AlertRule | null>;
|
|
1041
|
-
deleteRule(id: string, projectId: string): Promise<boolean>;
|
|
1042
|
-
listEnabledRules(): Promise<AlertRule[]>;
|
|
1043
|
-
getState(ruleId: string, projectId: string): Promise<AlertStateRow | null>;
|
|
1044
|
-
/**
|
|
1045
|
-
* Record a state transition with its notification still outstanding
|
|
1046
|
-
* (`notified = 0`); `markNotified` flips it once a channel accepts the
|
|
1047
|
-
* alert. On a database that has not applied migration 049 the column is
|
|
1048
|
-
* missing: the transition is still written (legacy statement) so alerting
|
|
1049
|
-
* keeps working through a rolling deploy, and the evaluator treats a row
|
|
1050
|
-
* without the column as delivered.
|
|
1051
|
-
*/
|
|
1052
|
-
transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
|
|
1053
|
-
/**
|
|
1054
|
-
* Mark the transition `(state, lastStateChange)` as delivered. Scoped to
|
|
1055
|
-
* the exact transition the caller notified for: if the rule changed state
|
|
1056
|
-
* again while the webhook was in flight, the newer transition keeps its
|
|
1057
|
-
* own outstanding flag. Returns false when nothing matched (stale or
|
|
1058
|
-
* already marked). On a pre-049 schema there is nothing to mark.
|
|
1059
|
-
*/
|
|
1060
|
-
markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
|
|
1061
|
-
recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean): Promise<void>;
|
|
1062
|
-
listEvaluations(params: {
|
|
1063
|
-
ruleId: string;
|
|
1064
|
-
projectId?: string;
|
|
1065
|
-
hours: number;
|
|
1066
|
-
limit?: number;
|
|
1067
|
-
}): Promise<AlertEvaluationsListResponse>;
|
|
1068
|
-
evaluateRule(rule: AlertRule): Promise<number>;
|
|
1069
|
-
/**
|
|
1070
|
-
* Stage 6 — fetch the latest narrative associated with this rule's
|
|
1071
|
-
* bound analysis. Used by the evaluator when firing an analysis-bound
|
|
1072
|
-
* alert so the webhook payload includes the human-readable
|
|
1073
|
-
* narrative, not just a threshold crossing.
|
|
1074
|
-
*/
|
|
1075
|
-
getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
|
|
1076
|
-
narrative: string | null;
|
|
1077
|
-
status: string | null;
|
|
1078
|
-
} | null>;
|
|
1079
|
-
private evaluateSpanRule;
|
|
1080
|
-
private evaluateLogRule;
|
|
1081
|
-
private evaluateUsageRule;
|
|
1082
|
-
private evaluateAIRule;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
1197
|
declare function recordChangeEvent(db: SqlDb, projectId: string, input: ChangeEventInput): Promise<ChangeEvent>;
|
|
1086
1198
|
declare function listChangeEvents(db: SqlDb, options: {
|
|
1087
1199
|
projectId: string;
|
|
@@ -1698,6 +1810,103 @@ declare class EvalCasesStore {
|
|
|
1698
1810
|
private buildMetadata;
|
|
1699
1811
|
}
|
|
1700
1812
|
|
|
1813
|
+
/**
|
|
1814
|
+
* `metrics` alert signal: validation and evaluation of an
|
|
1815
|
+
* `AlertQueryMetrics` against `metric_series` / `metric_point`.
|
|
1816
|
+
*
|
|
1817
|
+
* Evaluation, per selector (numerator, then the optional denominator):
|
|
1818
|
+
*
|
|
1819
|
+
* 1. Series: `metric_series` rows of the project whose name is one of the
|
|
1820
|
+
* selector's names (and whose `service_name` matches, when given), kept
|
|
1821
|
+
* when every listed data-point / resource attribute equals the rule's
|
|
1822
|
+
* value. Attribute values are compared as strings, so `200` matches a
|
|
1823
|
+
* stored `"200"` and vice versa.
|
|
1824
|
+
* 2. Points: the series' points whose `received_at` (the collector's clock,
|
|
1825
|
+
* as for every other alert signal) is inside the rule window, ordered by
|
|
1826
|
+
* producer time (`ts_ns`). The number read is `field` (`value`, or a
|
|
1827
|
+
* histogram/summary `count` / `sum`); points without it are skipped.
|
|
1828
|
+
* 3. Staleness: for `last`, `min`, `max`, `avg` and `sum` a series takes
|
|
1829
|
+
* part only when its newest point is at most `staleSecs` (default 300)
|
|
1830
|
+
* old, so a series that stopped reporting (a renamed host, a replaced
|
|
1831
|
+
* pod) no longer adds its last value. `increase` / `rate` keep every
|
|
1832
|
+
* series with points in the window: growth that happened is real even
|
|
1833
|
+
* if the series stopped afterwards.
|
|
1834
|
+
* 4. Each series is reduced on its own (`reduceSeries`), and the per-series
|
|
1835
|
+
* results are summed. A series with no usable point adds nothing.
|
|
1836
|
+
*
|
|
1837
|
+
* When no series takes part (in the numerator or the denominator) the
|
|
1838
|
+
* result is "no data": the value is 0 and `noData` is true. The rule's
|
|
1839
|
+
* `noData` policy decides what that means (`zero`: evaluate 0 as usual;
|
|
1840
|
+
* `keep`: leave the alert state as it is).
|
|
1841
|
+
*
|
|
1842
|
+
* `age_seconds` ignores the window: it is the seconds since the newest
|
|
1843
|
+
* `received_at` of any matching series. When no matching series has any
|
|
1844
|
+
* stored point (never reported, or everything aged out of retention) it is
|
|
1845
|
+
* the seconds since the rule was created, a finite number that grows until
|
|
1846
|
+
* the threshold is crossed, so "never reported" fires after the same grace
|
|
1847
|
+
* period as "stopped reporting".
|
|
1848
|
+
*
|
|
1849
|
+
* `age_seconds` is never "no data".
|
|
1850
|
+
*
|
|
1851
|
+
* Ratio: with a `denominator`, value = numerator / denominator when the
|
|
1852
|
+
* denominator is > 0 and >= minDenominator (default 0), else 0.
|
|
1853
|
+
*
|
|
1854
|
+
* Cost bounds: at most `MAX_MATCHED_SERIES` series and `MAX_LOADED_POINTS`
|
|
1855
|
+
* points per selector; past either the evaluation fails with an error (the
|
|
1856
|
+
* evaluator records it as an `error` evaluation) rather than returning a
|
|
1857
|
+
* value computed from part of the data. Metric rule windows are at most
|
|
1858
|
+
* `MAX_METRIC_WINDOW_MINS`.
|
|
1859
|
+
*/
|
|
1860
|
+
|
|
1861
|
+
/** Throws an Error with a user-facing message when `query` is malformed. */
|
|
1862
|
+
declare function validateMetricQuery(query: unknown): void;
|
|
1863
|
+
interface MetricPointSample {
|
|
1864
|
+
tsNs: string;
|
|
1865
|
+
startTsNs: string | null;
|
|
1866
|
+
value: number | null;
|
|
1867
|
+
count: number | null;
|
|
1868
|
+
sum: number | null;
|
|
1869
|
+
receivedAt: string;
|
|
1870
|
+
}
|
|
1871
|
+
interface SeriesReduceOptions {
|
|
1872
|
+
/**
|
|
1873
|
+
* The series has no history before the window (its `metric_series` row
|
|
1874
|
+
* was created inside it): a cumulative counter's first point then counts
|
|
1875
|
+
* as growth from 0 instead of being the baseline.
|
|
1876
|
+
*/
|
|
1877
|
+
bornInWindow?: boolean;
|
|
1878
|
+
aggregation: Exclude<AlertMetricAggregation, "age_seconds">;
|
|
1879
|
+
field: AlertMetricField;
|
|
1880
|
+
/** `metric_series.temporality`: 1 delta, 2 cumulative, null for gauges. */
|
|
1881
|
+
temporality: number | null;
|
|
1882
|
+
/** Start of the window in Unix nanoseconds. */
|
|
1883
|
+
windowStartNs: bigint;
|
|
1884
|
+
windowSecs: number;
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Reduces one series' window points to a number, or null when the series
|
|
1888
|
+
* has no point carrying `field` in the window.
|
|
1889
|
+
*/
|
|
1890
|
+
declare function reduceSeries(points: MetricPointSample[], opts: SeriesReduceOptions): number | null;
|
|
1891
|
+
interface MetricEvalContext {
|
|
1892
|
+
projectId: string;
|
|
1893
|
+
windowMins: number;
|
|
1894
|
+
/** The rule's `createdAt`; the reference for "never reported". */
|
|
1895
|
+
ruleCreatedAt: string;
|
|
1896
|
+
now: Date;
|
|
1897
|
+
/** Staleness bound for gauge-style aggregations; default 300. */
|
|
1898
|
+
staleSecs?: number;
|
|
1899
|
+
/** Point cap per selector; default MAX_LOADED_POINTS (tests lower it). */
|
|
1900
|
+
maxLoadedPoints?: number;
|
|
1901
|
+
}
|
|
1902
|
+
interface MetricEvaluation {
|
|
1903
|
+
value: number;
|
|
1904
|
+
/** No series took part; see the module comment. Never true for age_seconds. */
|
|
1905
|
+
noData: boolean;
|
|
1906
|
+
}
|
|
1907
|
+
/** Evaluates a validated metrics query to a finite number. */
|
|
1908
|
+
declare function evaluateMetricQuery(db: SqlDb, query: AlertQueryMetrics, ctx: MetricEvalContext): Promise<MetricEvaluation>;
|
|
1909
|
+
|
|
1701
1910
|
type MetricType = "gauge" | "sum" | "histogram" | "exp_histogram" | "summary";
|
|
1702
1911
|
/**
|
|
1703
1912
|
* A metric data point normalized for storage. Series-identifying fields
|
|
@@ -1855,4 +2064,4 @@ declare const _default: hono.Hono<{
|
|
|
1855
2064
|
Bindings: CollectorEnv;
|
|
1856
2065
|
}, hono_types.BlankSchema, "/">;
|
|
1857
2066
|
|
|
1858
|
-
export { type ActionGraphPurgeSummary, type ActionGraphRetentionOptions, type ActionRecord, ActionStore, type AgentRunRecord, AlertsStore, type AnalysesRunSummary, type ArtifactRecord, type BlobListOptions, type BlobListResult, type BlobObject, type BlobPutOptions, type BlobStore, BlobStoreToR2Adapter, type CollectorAuthConfig, type CollectorConfig, CollectorEnv, type CollectorPlugin, CollectorRouteContext, CollectorRuntime, CounterfactualVerificationError, D1Adapter, type DashboardAuthConfig, type EvalCase, type EvalCaseInput, type EvalCaseListOptions, type EvalCaseSourceLinks, EvalCaseSourceNotFoundError, type EvalCaseSourceType, EvalCasesStore, type EvalResultRecord, type EvalRun, type EvalRunCandidate, type EvalRunInput, type EvalRunSourceCase, type EvalRunStatus, type EvalRunsListOptions, type HostLookup, Logger, MetricsStore, PostgresAdapter, type PostgresAdapterOptions, type PostgresTypeParserRegistry, ProjectsStore, R2BlobStore, RemediationGateError, type RetentionCleanupSummary, type RetrievalEventRecord, S3BlobStore, type S3BlobStoreOptions, type ScheduledTaskHandler, type SpanProcessorPlugin, type SqlDb, type SqlDbFactory, type SqlStatement, type TailEvent, TailHub, type TailKind, type ToolCallRecord, type UsageEventProcessorPlugin, type WatchdogEvaluationSummary, acquireIncidentLease, actionRetentionCutoff, actionRoutesPlugin, aiReceiverPlugin, aiSpanPayloadsProcessorPlugin, alertsRoutesPlugin, allPlugins, analysesRoutesPlugin, approveRemediationProposal, askRoutesPlugin, autosreRoutesPlugin, botFilterPlugin, claimRemediationGrant, compareValue, completeRemediationProposal, computeChangeBlastRadius, computeTarjanScc, configurePostgresTypeParsers, connectedRoutesPlugin, createAlertEvaluatorHandler, createAnalysesRunHandler, createDashboardAuth, createDefaultCollectorApp, createRetentionCleanupHandler, createTelemetryCollectorApp, dashboardRoutesPlugin, _default as default, defaultSpanEnrichmentPlugin, dispatchIncidentWebhook, evalCasesRoutesPlugin, evaluateAllRules, evaluateRemediationWatchdogs, evidenceRetrievalRoutesPlugin, extractSpanLineageAndTopology, failRemediationProposal, findCrossTraceContention, genAiNormalizerPlugin, getChangeEvent, getIncidentInvestigation, getInvestigationFinding, getOntologyGraph, getRemediationProposal, identityReceiverPlugin, isEvalCaseSourceType, isEvalRunStatus, issueEnrichmentPlugin, issueInsightsPlugin, listChangeEvents, listIncidentInvestigations, listRemediationProposals, loadTraceFaultLocusFromDb, localizeTraceFaultLocus, logsReceiverPlugin, metricsReceiverPlugin, onboardingRoutesPlugin, openOrDeduplicateIncident, otlpReceiverPlugin, platformRoutesPlugin, profileRoutesPlugin, projectsRoutesPlugin, proposeRemediationAction, purgeExpiredActionGraph, queryRoutesPlugin, recordChangeEvent, recordInvestigationFinding, redactionProcessorPlugin, rejectRemediationProposal, replayQueryRoutesPlugin, replayReceiverPlugin, searchInvestigationFindings, tailRoutesPlugin, timelineRoutesPlugin, uaEnrichmentPlugin, upsertOntologyFromSpans, usageNormalizationPlugin, usagePrivacyPlugin, usageQueryRoutesPlugin, usageReceiverPlugin, usersQueryRoutesPlugin, utmEnrichmentPlugin, verifyPairedCounterfactualProof };
|
|
2067
|
+
export { type ActionGraphPurgeSummary, type ActionGraphRetentionOptions, type ActionRecord, ActionStore, type AgentRunRecord, AlertEmailTransport, AlertsStore, type AnalysesRunSummary, type ArtifactRecord, type BlobListOptions, type BlobListResult, type BlobObject, type BlobPutOptions, type BlobStore, BlobStoreToR2Adapter, type CollectorAuthConfig, type CollectorConfig, CollectorEnv, type CollectorPlugin, CollectorRouteContext, CollectorRuntime, CounterfactualVerificationError, D1Adapter, type DashboardAuthConfig, type EvalCase, type EvalCaseInput, type EvalCaseListOptions, type EvalCaseSourceLinks, EvalCaseSourceNotFoundError, type EvalCaseSourceType, EvalCasesStore, type EvalResultRecord, type EvalRun, type EvalRunCandidate, type EvalRunInput, type EvalRunSourceCase, type EvalRunStatus, type EvalRunsListOptions, type HostLookup, Logger, MetricsStore, PostgresAdapter, type PostgresAdapterOptions, type PostgresTypeParserRegistry, ProjectsStore, R2BlobStore, RemediationGateError, type RetentionCleanupSummary, type RetrievalEventRecord, S3BlobStore, type S3BlobStoreOptions, type ScheduledTaskHandler, type SpanProcessorPlugin, type SqlDb, type SqlDbFactory, type SqlStatement, type TailEvent, TailHub, type TailKind, type ToolCallRecord, type UsageEventProcessorPlugin, type WatchdogEvaluationSummary, acquireIncidentLease, actionRetentionCutoff, actionRoutesPlugin, aiReceiverPlugin, aiSpanPayloadsProcessorPlugin, alertsRoutesPlugin, allPlugins, analysesRoutesPlugin, approveRemediationProposal, askRoutesPlugin, autosreRoutesPlugin, botFilterPlugin, claimRemediationGrant, compareValue, completeRemediationProposal, computeChangeBlastRadius, computeTarjanScc, configurePostgresTypeParsers, connectedRoutesPlugin, createAlertEvaluatorHandler, createAnalysesRunHandler, createDashboardAuth, createDefaultCollectorApp, createRetentionCleanupHandler, createTelemetryCollectorApp, dashboardRoutesPlugin, _default as default, defaultSpanEnrichmentPlugin, dispatchIncidentWebhook, evalCasesRoutesPlugin, evaluateAllRules, evaluateMetricQuery, evaluateRemediationWatchdogs, evaluateRuleBatch, evidenceRetrievalRoutesPlugin, extractSpanLineageAndTopology, failRemediationProposal, findCrossTraceContention, genAiNormalizerPlugin, getChangeEvent, getIncidentInvestigation, getInvestigationFinding, getOntologyGraph, getRemediationProposal, identityReceiverPlugin, isEvalCaseSourceType, isEvalRunStatus, issueEnrichmentPlugin, issueInsightsPlugin, listChangeEvents, listIncidentInvestigations, listRemediationProposals, loadTraceFaultLocusFromDb, localizeTraceFaultLocus, logsReceiverPlugin, metricsReceiverPlugin, onboardingRoutesPlugin, openOrDeduplicateIncident, otlpReceiverPlugin, platformRoutesPlugin, profileRoutesPlugin, projectsRoutesPlugin, proposeRemediationAction, purgeExpiredActionGraph, queryRoutesPlugin, recordChangeEvent, recordInvestigationFinding, redactionProcessorPlugin, reduceSeries, rejectRemediationProposal, replayQueryRoutesPlugin, replayReceiverPlugin, searchInvestigationFindings, tailRoutesPlugin, timelineRoutesPlugin, uaEnrichmentPlugin, upsertOntologyFromSpans, usageNormalizationPlugin, usagePrivacyPlugin, usageQueryRoutesPlugin, usageReceiverPlugin, usersQueryRoutesPlugin, utmEnrichmentPlugin, validateMetricQuery, verifyPairedCounterfactualProof };
|