@schedulespark/observability 0.0.2-beta-1-0-0

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +384 -0
  4. package/SECURITY_REVIEW.md +90 -0
  5. package/dist/alerts/dispatch.d.ts +6 -0
  6. package/dist/alerts/dispatch.js +11 -0
  7. package/dist/alerts/email.d.ts +37 -0
  8. package/dist/alerts/email.js +29 -0
  9. package/dist/alerts/format.d.ts +12 -0
  10. package/dist/alerts/format.js +30 -0
  11. package/dist/alerts/index.d.ts +8 -0
  12. package/dist/alerts/index.js +4 -0
  13. package/dist/alerts/notifier.d.ts +15 -0
  14. package/dist/alerts/notifier.js +31 -0
  15. package/dist/alerts/spike.d.ts +25 -0
  16. package/dist/alerts/spike.js +41 -0
  17. package/dist/alerts/types.d.ts +38 -0
  18. package/dist/alerts/types.js +1 -0
  19. package/dist/alerts/webhook.d.ts +14 -0
  20. package/dist/alerts/webhook.js +59 -0
  21. package/dist/browser/client.d.ts +26 -0
  22. package/dist/browser/client.js +131 -0
  23. package/dist/browser/index.d.ts +3 -0
  24. package/dist/browser/index.js +1 -0
  25. package/dist/cli/bin.d.ts +2 -0
  26. package/dist/cli/bin.js +6 -0
  27. package/dist/cli/index.d.ts +10 -0
  28. package/dist/cli/index.js +45 -0
  29. package/dist/cli/migrate.d.ts +6 -0
  30. package/dist/cli/migrate.js +14 -0
  31. package/dist/cli/options.d.ts +17 -0
  32. package/dist/cli/options.js +28 -0
  33. package/dist/cli/projects.d.ts +6 -0
  34. package/dist/cli/projects.js +52 -0
  35. package/dist/cli/prune.d.ts +8 -0
  36. package/dist/cli/prune.js +32 -0
  37. package/dist/cli/rollup.d.ts +6 -0
  38. package/dist/cli/rollup.js +24 -0
  39. package/dist/cli/serve.d.ts +5 -0
  40. package/dist/cli/serve.js +72 -0
  41. package/dist/cli/sourcemap.d.ts +8 -0
  42. package/dist/cli/sourcemap.js +113 -0
  43. package/dist/core/breadcrumbs.d.ts +21 -0
  44. package/dist/core/breadcrumbs.js +20 -0
  45. package/dist/core/fingerprint.d.ts +10 -0
  46. package/dist/core/fingerprint.js +35 -0
  47. package/dist/core/index.d.ts +4 -0
  48. package/dist/core/index.js +4 -0
  49. package/dist/core/types.d.ts +219 -0
  50. package/dist/core/types.js +35 -0
  51. package/dist/dashboard/core.d.ts +67 -0
  52. package/dist/dashboard/core.js +40 -0
  53. package/dist/dashboard/fastify.d.ts +17 -0
  54. package/dist/dashboard/fastify.js +247 -0
  55. package/dist/dashboard/html-secondary.d.ts +15 -0
  56. package/dist/dashboard/html-secondary.js +85 -0
  57. package/dist/dashboard/html-shared.d.ts +14 -0
  58. package/dist/dashboard/html-shared.js +39 -0
  59. package/dist/dashboard/html.d.ts +18 -0
  60. package/dist/dashboard/html.js +161 -0
  61. package/dist/dashboard/index.d.ts +5 -0
  62. package/dist/dashboard/index.js +3 -0
  63. package/dist/node/client.d.ts +38 -0
  64. package/dist/node/client.js +191 -0
  65. package/dist/node/exception.d.ts +15 -0
  66. package/dist/node/exception.js +33 -0
  67. package/dist/node/fastify.d.ts +7 -0
  68. package/dist/node/fastify.js +20 -0
  69. package/dist/node/index.d.ts +11 -0
  70. package/dist/node/index.js +5 -0
  71. package/dist/node/logs.d.ts +23 -0
  72. package/dist/node/logs.js +90 -0
  73. package/dist/node/metrics.d.ts +27 -0
  74. package/dist/node/metrics.js +18 -0
  75. package/dist/node/prisma.d.ts +63 -0
  76. package/dist/node/prisma.js +53 -0
  77. package/dist/node/queue.d.ts +25 -0
  78. package/dist/node/queue.js +42 -0
  79. package/dist/node/redact.d.ts +7 -0
  80. package/dist/node/redact.js +26 -0
  81. package/dist/node/tracing.d.ts +28 -0
  82. package/dist/node/tracing.js +49 -0
  83. package/dist/storage/comments.d.ts +13 -0
  84. package/dist/storage/comments.js +20 -0
  85. package/dist/storage/index.d.ts +31 -0
  86. package/dist/storage/index.js +33 -0
  87. package/dist/storage/logs.d.ts +19 -0
  88. package/dist/storage/logs.js +35 -0
  89. package/dist/storage/metrics-ingest.d.ts +14 -0
  90. package/dist/storage/metrics-ingest.js +16 -0
  91. package/dist/storage/metrics-rollup.d.ts +38 -0
  92. package/dist/storage/metrics-rollup.js +80 -0
  93. package/dist/storage/metrics.d.ts +7 -0
  94. package/dist/storage/metrics.js +10 -0
  95. package/dist/storage/migrations.d.ts +17 -0
  96. package/dist/storage/migrations.js +238 -0
  97. package/dist/storage/pool.d.ts +30 -0
  98. package/dist/storage/pool.js +28 -0
  99. package/dist/storage/projects.d.ts +17 -0
  100. package/dist/storage/projects.js +31 -0
  101. package/dist/storage/queries.d.ts +48 -0
  102. package/dist/storage/queries.js +146 -0
  103. package/dist/storage/retention.d.ts +28 -0
  104. package/dist/storage/retention.js +29 -0
  105. package/dist/storage/rows.d.ts +151 -0
  106. package/dist/storage/rows.js +134 -0
  107. package/dist/storage/saved-views.d.ts +15 -0
  108. package/dist/storage/saved-views.js +28 -0
  109. package/dist/storage/schema-ident.d.ts +7 -0
  110. package/dist/storage/schema-ident.js +13 -0
  111. package/dist/storage/spans.d.ts +14 -0
  112. package/dist/storage/spans.js +21 -0
  113. package/docs/screenshots/dashboard.jpg +0 -0
  114. package/docs/standalone-example.md +110 -0
  115. package/package.json +102 -0
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Recursively redacts values whose key looks like a secret (authorization headers,
3
+ * cookies, passwords, tokens, API keys). Applied by default to any context object
4
+ * before it's persisted, since events are written straight into the host's own
5
+ * database.
6
+ */
7
+ export declare function redactSecrets<T>(value: T): T;
@@ -0,0 +1,26 @@
1
+ const SECRET_KEY_PATTERN = /authorization|cookie|password|secret|token|api[-_]?key/i;
2
+ /**
3
+ * Recursively redacts values whose key looks like a secret (authorization headers,
4
+ * cookies, passwords, tokens, API keys). Applied by default to any context object
5
+ * before it's persisted, since events are written straight into the host's own
6
+ * database.
7
+ */
8
+ export function redactSecrets(value) {
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => redactSecrets(item));
11
+ }
12
+ if (value !== null && typeof value === "object") {
13
+ return redactObject(value);
14
+ }
15
+ return value;
16
+ }
17
+ /**
18
+ * Redacts secret-looking keys on a plain object, recursing into nested values.
19
+ */
20
+ function redactObject(input) {
21
+ const result = {};
22
+ for (const [key, val] of Object.entries(input)) {
23
+ result[key] = SECRET_KEY_PATTERN.test(key) ? "[redacted]" : redactSecrets(val);
24
+ }
25
+ return result;
26
+ }
@@ -0,0 +1,28 @@
1
+ import type { CaptureQueue } from "./queue.js";
2
+ import type { RecordedSpan, SpanStatus } from "../core/index.js";
3
+ /**
4
+ * A child operation within a transaction. Basic tracing only supports one level of
5
+ * nesting — a span can't start further spans of its own.
6
+ */
7
+ export interface Span {
8
+ finish: (status?: SpanStatus) => void;
9
+ }
10
+ /**
11
+ * A root timed operation (e.g. one tRPC procedure call, one HTTP request).
12
+ */
13
+ export interface Transaction {
14
+ startSpan: (name: string, tags?: Record<string, string>) => Span;
15
+ finish: (status?: SpanStatus) => void;
16
+ }
17
+ /**
18
+ * Starts new transactions, all sharing one underlying span queue.
19
+ */
20
+ export interface Tracer {
21
+ startTransaction: (name: string, tags?: Record<string, string>) => Transaction;
22
+ }
23
+ /**
24
+ * Creates a tracer bound to a queue: `startTransaction` builds transactions/spans
25
+ * that enqueue themselves on `finish()`, batched and flushed the same non-blocking
26
+ * way captured events are.
27
+ */
28
+ export declare function createTracer(queue: CaptureQueue<RecordedSpan>): Tracer;
@@ -0,0 +1,49 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const DEFAULT_STATUS = "ok";
3
+ /**
4
+ * Creates a tracer bound to a queue: `startTransaction` builds transactions/spans
5
+ * that enqueue themselves on `finish()`, batched and flushed the same non-blocking
6
+ * way captured events are.
7
+ */
8
+ export function createTracer(queue) {
9
+ return {
10
+ startTransaction(name, tags) {
11
+ const traceId = randomUUID();
12
+ return buildTimedOperation(queue, { traceId, parentId: null, name, tags });
13
+ }
14
+ };
15
+ }
16
+ /**
17
+ * Builds a span-like object (used for both transactions and their child spans):
18
+ * tracks its own start time and enqueues a `RecordedSpan` on `finish()`.
19
+ */
20
+ function buildTimedOperation(queue, input) {
21
+ const id = randomUUID();
22
+ const startedAt = new Date();
23
+ const start = Date.now();
24
+ return {
25
+ startSpan(name, tags) {
26
+ return buildChildSpan(queue, { traceId: input.traceId, parentId: id, name, tags });
27
+ },
28
+ finish(status = DEFAULT_STATUS) {
29
+ queue.enqueue({
30
+ id,
31
+ traceId: input.traceId,
32
+ parentId: input.parentId,
33
+ name: input.name,
34
+ status,
35
+ startedAt: startedAt.toISOString(),
36
+ durationMs: Date.now() - start,
37
+ tags: input.tags ?? {}
38
+ });
39
+ }
40
+ };
41
+ }
42
+ /**
43
+ * Builds a leaf span — like `buildTimedOperation`, but without its own `startSpan`,
44
+ * since basic tracing caps nesting at one level.
45
+ */
46
+ function buildChildSpan(queue, input) {
47
+ const { finish } = buildTimedOperation(queue, input);
48
+ return { finish };
49
+ }
@@ -0,0 +1,13 @@
1
+ import type { StorageHandle } from "./pool.js";
2
+ import type { Comment } from "../core/index.js";
3
+ /**
4
+ * Adds a free-text comment to an issue.
5
+ */
6
+ export declare function addComment(handle: StorageHandle, issueId: string, input: {
7
+ author: string;
8
+ body: string;
9
+ }): Promise<Comment>;
10
+ /**
11
+ * Lists an issue's comments, oldest first.
12
+ */
13
+ export declare function listComments(handle: StorageHandle, issueId: string): Promise<Comment[]>;
@@ -0,0 +1,20 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mapCommentRow } from "./rows.js";
3
+ /**
4
+ * Adds a free-text comment to an issue.
5
+ */
6
+ export async function addComment(handle, issueId, input) {
7
+ await handle.ready;
8
+ const { rows } = await handle.pool.query(`INSERT INTO ${handle.quotedSchema}.comments (id, issue_id, author, body, created_at)
9
+ VALUES ($1, $2, $3, $4, now())
10
+ RETURNING *`, [randomUUID(), issueId, input.author, input.body]);
11
+ return mapCommentRow(rows[0]);
12
+ }
13
+ /**
14
+ * Lists an issue's comments, oldest first.
15
+ */
16
+ export async function listComments(handle, issueId) {
17
+ await handle.ready;
18
+ const { rows } = await handle.pool.query(`SELECT * FROM ${handle.quotedSchema}.comments WHERE issue_id = $1 ORDER BY created_at ASC`, [issueId]);
19
+ return rows.map(mapCommentRow);
20
+ }
@@ -0,0 +1,31 @@
1
+ import type { StorageHandle, StorageOptions } from "./pool.js";
2
+ export { closeStorage, createStorage } from "./pool.js";
3
+ export { migrate } from "./migrations.js";
4
+ export { addComment, listComments } from "./comments.js";
5
+ export { recordLog, listLogs } from "./logs.js";
6
+ export { countEventsSince } from "./metrics.js";
7
+ export { recordMetricPoint } from "./metrics-ingest.js";
8
+ export { listMetricRollups, rollupMetrics } from "./metrics-rollup.js";
9
+ export { createProject, findProjectByApiKey, listProjects } from "./projects.js";
10
+ export { assignIssue, getIssueWithEvents, listIssues, recordEvent, updateIssueStatus } from "./queries.js";
11
+ export { pruneEvents, pruneLogs } from "./retention.js";
12
+ export { createSavedView, deleteSavedView, listSavedViews } from "./saved-views.js";
13
+ export { listTransactions, recordSpan } from "./spans.js";
14
+ export type { StorageHandle, StorageOptions } from "./pool.js";
15
+ export type { ListMetricRollupsOptions, RollupMetricsOptions, RollupMetricsResult } from "./metrics-rollup.js";
16
+ export type { RecordEventResult } from "./queries.js";
17
+ export type { PruneLogsResult, PruneResult } from "./retention.js";
18
+ /**
19
+ * Opens a storage handle and waits for its migrations to finish before returning it.
20
+ * Prefer this over `createStorage` when you'd rather surface a migration failure
21
+ * immediately (e.g. the standalone CLI) instead of letting it happen in the
22
+ * background — `createStorage` already migrates in the background either way, so
23
+ * this is just `createStorage` plus an explicit `await handle.ready`.
24
+ */
25
+ export declare function initStorage(options: StorageOptions): Promise<StorageHandle>;
26
+ /**
27
+ * Waits for an already-open storage handle's migrations to finish. Useful when a
28
+ * host application wants to log a migration failure without blocking route
29
+ * registration on it.
30
+ */
31
+ export declare function migrateHandle(handle: StorageHandle): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { createStorage } from "./pool.js";
2
+ export { closeStorage, createStorage } from "./pool.js";
3
+ export { migrate } from "./migrations.js";
4
+ export { addComment, listComments } from "./comments.js";
5
+ export { recordLog, listLogs } from "./logs.js";
6
+ export { countEventsSince } from "./metrics.js";
7
+ export { recordMetricPoint } from "./metrics-ingest.js";
8
+ export { listMetricRollups, rollupMetrics } from "./metrics-rollup.js";
9
+ export { createProject, findProjectByApiKey, listProjects } from "./projects.js";
10
+ export { assignIssue, getIssueWithEvents, listIssues, recordEvent, updateIssueStatus } from "./queries.js";
11
+ export { pruneEvents, pruneLogs } from "./retention.js";
12
+ export { createSavedView, deleteSavedView, listSavedViews } from "./saved-views.js";
13
+ export { listTransactions, recordSpan } from "./spans.js";
14
+ /**
15
+ * Opens a storage handle and waits for its migrations to finish before returning it.
16
+ * Prefer this over `createStorage` when you'd rather surface a migration failure
17
+ * immediately (e.g. the standalone CLI) instead of letting it happen in the
18
+ * background — `createStorage` already migrates in the background either way, so
19
+ * this is just `createStorage` plus an explicit `await handle.ready`.
20
+ */
21
+ export async function initStorage(options) {
22
+ const handle = createStorage(options);
23
+ await handle.ready;
24
+ return handle;
25
+ }
26
+ /**
27
+ * Waits for an already-open storage handle's migrations to finish. Useful when a
28
+ * host application wants to log a migration failure without blocking route
29
+ * registration on it.
30
+ */
31
+ export function migrateHandle(handle) {
32
+ return handle.ready;
33
+ }
@@ -0,0 +1,19 @@
1
+ import type { StorageHandle } from "./pool.js";
2
+ import type { LogEntry, LogLevel } from "../core/index.js";
3
+ /**
4
+ * Records a single structured log line. Defaults to the `"default"` project like
5
+ * every other capture path when the caller doesn't configure multi-project support.
6
+ */
7
+ export declare function recordLog(handle: StorageHandle, input: {
8
+ level: LogLevel;
9
+ message: string;
10
+ context?: Record<string, unknown>;
11
+ }, projectId?: string): Promise<LogEntry>;
12
+ /**
13
+ * Lists the most recently logged lines, optionally filtered by level and/or project.
14
+ */
15
+ export declare function listLogs(handle: StorageHandle, options?: {
16
+ level?: LogLevel;
17
+ projectId?: string;
18
+ limit?: number;
19
+ }): Promise<LogEntry[]>;
@@ -0,0 +1,35 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { DEFAULT_PROJECT_ID } from "../core/index.js";
3
+ import { mapLogRow } from "./rows.js";
4
+ const DEFAULT_LOG_LIST_LIMIT = 100;
5
+ /**
6
+ * Records a single structured log line. Defaults to the `"default"` project like
7
+ * every other capture path when the caller doesn't configure multi-project support.
8
+ */
9
+ export async function recordLog(handle, input, projectId = DEFAULT_PROJECT_ID) {
10
+ await handle.ready;
11
+ const { rows } = await handle.pool.query(`INSERT INTO ${handle.quotedSchema}.logs (id, project_id, level, message, context, logged_at)
12
+ VALUES ($1, $2, $3, $4, $5, now())
13
+ RETURNING *`, [randomUUID(), projectId, input.level, input.message, input.context ?? {}]);
14
+ return mapLogRow(rows[0]);
15
+ }
16
+ /**
17
+ * Lists the most recently logged lines, optionally filtered by level and/or project.
18
+ */
19
+ export async function listLogs(handle, options = {}) {
20
+ await handle.ready;
21
+ const params = [];
22
+ const conditions = [];
23
+ if (options.projectId) {
24
+ params.push(options.projectId);
25
+ conditions.push(`project_id = $${String(params.length)}`);
26
+ }
27
+ if (options.level) {
28
+ params.push(options.level);
29
+ conditions.push(`level = $${String(params.length)}`);
30
+ }
31
+ params.push(options.limit ?? DEFAULT_LOG_LIST_LIMIT);
32
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
33
+ const { rows } = await handle.pool.query(`SELECT * FROM ${handle.quotedSchema}.logs ${where} ORDER BY logged_at DESC LIMIT $${String(params.length)}`, params);
34
+ return rows.map(mapLogRow);
35
+ }
@@ -0,0 +1,14 @@
1
+ import type { StorageHandle } from "./pool.js";
2
+ import type { MetricKind, MetricPoint } from "../core/index.js";
3
+ /**
4
+ * Records a single raw metric data point. Defaults to the `"default"` project like
5
+ * every other capture path. Named `metrics-ingest.ts` (not `metrics.ts`, which already
6
+ * exists for `countEventsSince`/spike alerting) to avoid colliding in both filename
7
+ * and intent with that unrelated module.
8
+ */
9
+ export declare function recordMetricPoint(handle: StorageHandle, point: {
10
+ name: string;
11
+ kind: MetricKind;
12
+ value: number;
13
+ tags?: Record<string, string>;
14
+ }, projectId?: string): Promise<MetricPoint>;
@@ -0,0 +1,16 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { DEFAULT_PROJECT_ID } from "../core/index.js";
3
+ import { mapMetricPointRow } from "./rows.js";
4
+ /**
5
+ * Records a single raw metric data point. Defaults to the `"default"` project like
6
+ * every other capture path. Named `metrics-ingest.ts` (not `metrics.ts`, which already
7
+ * exists for `countEventsSince`/spike alerting) to avoid colliding in both filename
8
+ * and intent with that unrelated module.
9
+ */
10
+ export async function recordMetricPoint(handle, point, projectId = DEFAULT_PROJECT_ID) {
11
+ await handle.ready;
12
+ const { rows } = await handle.pool.query(`INSERT INTO ${handle.quotedSchema}.metric_points_raw (id, project_id, name, kind, value, tags, recorded_at)
13
+ VALUES ($1, $2, $3, $4, $5, $6, now())
14
+ RETURNING *`, [randomUUID(), projectId, point.name, point.kind, point.value, point.tags ?? {}]);
15
+ return mapMetricPointRow(rows[0]);
16
+ }
@@ -0,0 +1,38 @@
1
+ import type { StorageHandle } from "./pool.js";
2
+ import type { MetricBucketSize, MetricRollup } from "../core/index.js";
3
+ /**
4
+ * Options controlling a single rollup run.
5
+ */
6
+ export interface RollupMetricsOptions {
7
+ bucketSizes?: MetricBucketSize[];
8
+ rawRetentionDays?: number;
9
+ }
10
+ /**
11
+ * Outcome of a single rollup run.
12
+ */
13
+ export interface RollupMetricsResult {
14
+ rolledUpBuckets: number;
15
+ prunedRawPoints: number;
16
+ }
17
+ /**
18
+ * Re-aggregates every raw metric point into `metric_rollups`, one bucket size at a
19
+ * time, then prunes raw points older than the retention window. Each bucket is a
20
+ * full replace (not an addition) of `sum`/`count`/`min`/`max` from the current raw
21
+ * data, so rerunning this job is always safe — it never double-counts a point that
22
+ * was already rolled up in a previous run.
23
+ */
24
+ export declare function rollupMetrics(handle: StorageHandle, options?: RollupMetricsOptions): Promise<RollupMetricsResult>;
25
+ /**
26
+ * Options for listing metric rollups.
27
+ */
28
+ export interface ListMetricRollupsOptions {
29
+ bucketSize?: MetricBucketSize;
30
+ projectId?: string;
31
+ limit?: number;
32
+ }
33
+ /**
34
+ * Lists the most recent rollup bucket per `(name, tags)` combination, optionally
35
+ * filtered by bucket size/project. Only the latest bucket per metric is returned —
36
+ * this backs a simple "current state" table view, not a time-series chart.
37
+ */
38
+ export declare function listMetricRollups(handle: StorageHandle, options?: ListMetricRollupsOptions): Promise<MetricRollup[]>;
@@ -0,0 +1,80 @@
1
+ import { mapMetricRollupRow } from "./rows.js";
2
+ const DEFAULT_BUCKET_SIZES = ["hour", "day"];
3
+ const DEFAULT_RAW_RETENTION_DAYS = 3;
4
+ const DEFAULT_ROLLUP_LIST_LIMIT = 200;
5
+ /**
6
+ * Re-aggregates every raw metric point into `metric_rollups`, one bucket size at a
7
+ * time, then prunes raw points older than the retention window. Each bucket is a
8
+ * full replace (not an addition) of `sum`/`count`/`min`/`max` from the current raw
9
+ * data, so rerunning this job is always safe — it never double-counts a point that
10
+ * was already rolled up in a previous run.
11
+ */
12
+ export async function rollupMetrics(handle, options = {}) {
13
+ await handle.ready;
14
+ const bucketSizes = options.bucketSizes ?? DEFAULT_BUCKET_SIZES;
15
+ const rawRetentionDays = options.rawRetentionDays ?? DEFAULT_RAW_RETENTION_DAYS;
16
+ if (!Number.isInteger(rawRetentionDays) || rawRetentionDays <= 0) {
17
+ throw new Error(`rawRetentionDays must be a positive integer, got ${String(rawRetentionDays)}`);
18
+ }
19
+ let rolledUpBuckets = 0;
20
+ for (const bucketSize of bucketSizes) {
21
+ rolledUpBuckets += await rollupBucketSize(handle, bucketSize);
22
+ }
23
+ const { rowCount } = await handle.pool.query(`DELETE FROM ${handle.quotedSchema}.metric_points_raw WHERE recorded_at < now() - ($1 || ' days')::interval`, [rawRetentionDays]);
24
+ return { rolledUpBuckets, prunedRawPoints: rowCount ?? 0 };
25
+ }
26
+ /**
27
+ * Rolls up every raw point into one bucket size, upserting each `(project, name,
28
+ * kind, tags, bucket)` combination.
29
+ */
30
+ async function rollupBucketSize(handle, bucketSize) {
31
+ const { rowCount } = await handle.pool.query(`INSERT INTO ${handle.quotedSchema}.metric_rollups
32
+ (id, project_id, name, kind, bucket_start, bucket_size, tags, sum, count, min, max)
33
+ SELECT
34
+ gen_random_uuid()::text,
35
+ project_id,
36
+ name,
37
+ kind,
38
+ date_trunc($1, recorded_at),
39
+ $1,
40
+ tags,
41
+ sum(value),
42
+ count(*),
43
+ min(value),
44
+ max(value)
45
+ FROM ${handle.quotedSchema}.metric_points_raw
46
+ GROUP BY project_id, name, kind, date_trunc($1, recorded_at), tags
47
+ ON CONFLICT (project_id, name, tags, bucket_start, bucket_size)
48
+ DO UPDATE SET
49
+ sum = EXCLUDED.sum,
50
+ count = EXCLUDED.count,
51
+ min = EXCLUDED.min,
52
+ max = EXCLUDED.max`, [bucketSize]);
53
+ return rowCount ?? 0;
54
+ }
55
+ /**
56
+ * Lists the most recent rollup bucket per `(name, tags)` combination, optionally
57
+ * filtered by bucket size/project. Only the latest bucket per metric is returned —
58
+ * this backs a simple "current state" table view, not a time-series chart.
59
+ */
60
+ export async function listMetricRollups(handle, options = {}) {
61
+ await handle.ready;
62
+ const conditions = [];
63
+ const params = [];
64
+ if (options.bucketSize) {
65
+ params.push(options.bucketSize);
66
+ conditions.push(`bucket_size = $${String(params.length)}`);
67
+ }
68
+ if (options.projectId) {
69
+ params.push(options.projectId);
70
+ conditions.push(`project_id = $${String(params.length)}`);
71
+ }
72
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
73
+ params.push(options.limit ?? DEFAULT_ROLLUP_LIST_LIMIT);
74
+ const { rows } = await handle.pool.query(`SELECT DISTINCT ON (name, tags) *
75
+ FROM ${handle.quotedSchema}.metric_rollups
76
+ ${where}
77
+ ORDER BY name, tags, bucket_start DESC
78
+ LIMIT $${String(params.length)}`, params);
79
+ return rows.map(mapMetricRollupRow);
80
+ }
@@ -0,0 +1,7 @@
1
+ import type { StorageHandle } from "./pool.js";
2
+ import type { EventLevel } from "../core/index.js";
3
+ /**
4
+ * Counts events at a given level captured within the last `windowMinutes`. Used by
5
+ * the spike alert monitor; not exposed in the dashboard yet.
6
+ */
7
+ export declare function countEventsSince(handle: StorageHandle, level: EventLevel, windowMinutes: number): Promise<number>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Counts events at a given level captured within the last `windowMinutes`. Used by
3
+ * the spike alert monitor; not exposed in the dashboard yet.
4
+ */
5
+ export async function countEventsSince(handle, level, windowMinutes) {
6
+ await handle.ready;
7
+ const { rows } = await handle.pool.query(`SELECT count(*)::text AS count FROM ${handle.quotedSchema}.events
8
+ WHERE level = $1 AND captured_at > now() - ($2 || ' minutes')::interval`, [level, windowMinutes]);
9
+ return Number(rows[0]?.count ?? "0");
10
+ }
@@ -0,0 +1,17 @@
1
+ import type { Pool } from "pg";
2
+ /**
3
+ * A single versioned, forward-only schema migration.
4
+ */
5
+ export interface Migration {
6
+ id: string;
7
+ sql: (quotedSchema: string) => string;
8
+ }
9
+ export declare const migrations: Migration[];
10
+ /**
11
+ * Ensures the observability schema, migration-tracking table, and all pending
12
+ * migrations exist. Safe to call concurrently from multiple pools/processes against
13
+ * the same database (e.g. a Node SDK client and a dashboard server booting at the same
14
+ * time): a Postgres advisory lock, scoped to the schema name, serializes migration
15
+ * runs so `CREATE SCHEMA IF NOT EXISTS` races can never produce a duplicate-key error.
16
+ */
17
+ export declare function migrate(pool: Pool, quotedSchema: string): Promise<void>;