pi-harness-runtime 1.1.396 → 1.1.398
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 +54 -7
- package/harness/notification-events.ts +6 -6
- package/index.ts +1 -1
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/README.md +41 -6
- package/packages/logging/dist/classification.d.ts +65 -0
- package/packages/logging/dist/classification.d.ts.map +1 -0
- package/packages/logging/dist/classification.js +155 -0
- package/packages/logging/dist/classification.js.map +1 -0
- package/packages/logging/dist/config.d.ts +9 -0
- package/packages/logging/dist/config.d.ts.map +1 -1
- package/packages/logging/dist/config.js +40 -3
- package/packages/logging/dist/config.js.map +1 -1
- package/packages/logging/dist/index.d.ts +9 -1
- package/packages/logging/dist/index.d.ts.map +1 -1
- package/packages/logging/dist/index.js +10 -1
- package/packages/logging/dist/index.js.map +1 -1
- package/packages/logging/dist/ingest/schema.d.ts +60 -0
- package/packages/logging/dist/ingest/schema.d.ts.map +1 -0
- package/packages/logging/dist/ingest/schema.js +245 -0
- package/packages/logging/dist/ingest/schema.js.map +1 -0
- package/packages/logging/dist/installation.d.ts +32 -0
- package/packages/logging/dist/installation.d.ts.map +1 -0
- package/packages/logging/dist/installation.js +64 -0
- package/packages/logging/dist/installation.js.map +1 -0
- package/packages/logging/dist/sensitive-keys.d.ts +12 -0
- package/packages/logging/dist/sensitive-keys.d.ts.map +1 -0
- package/packages/logging/dist/sensitive-keys.js +39 -0
- package/packages/logging/dist/sensitive-keys.js.map +1 -0
- package/packages/logging/dist/server/credentials.d.ts +85 -0
- package/packages/logging/dist/server/credentials.d.ts.map +1 -0
- package/packages/logging/dist/server/credentials.js +147 -0
- package/packages/logging/dist/server/credentials.js.map +1 -0
- package/packages/logging/dist/server/http.d.ts +36 -0
- package/packages/logging/dist/server/http.d.ts.map +1 -0
- package/packages/logging/dist/server/http.js +139 -0
- package/packages/logging/dist/server/http.js.map +1 -0
- package/packages/logging/dist/server/index-store.d.ts +95 -0
- package/packages/logging/dist/server/index-store.d.ts.map +1 -0
- package/packages/logging/dist/server/index-store.js +190 -0
- package/packages/logging/dist/server/index-store.js.map +1 -0
- package/packages/logging/dist/server/index.d.ts +22 -0
- package/packages/logging/dist/server/index.d.ts.map +1 -0
- package/packages/logging/dist/server/index.js +15 -0
- package/packages/logging/dist/server/index.js.map +1 -0
- package/packages/logging/dist/server/keys.d.ts +22 -0
- package/packages/logging/dist/server/keys.d.ts.map +1 -0
- package/packages/logging/dist/server/keys.js +24 -0
- package/packages/logging/dist/server/keys.js.map +1 -0
- package/packages/logging/dist/server/outbox-worker.d.ts +55 -0
- package/packages/logging/dist/server/outbox-worker.d.ts.map +1 -0
- package/packages/logging/dist/server/outbox-worker.js +95 -0
- package/packages/logging/dist/server/outbox-worker.js.map +1 -0
- package/packages/logging/dist/server/outbox.d.ts +112 -0
- package/packages/logging/dist/server/outbox.d.ts.map +1 -0
- package/packages/logging/dist/server/outbox.js +247 -0
- package/packages/logging/dist/server/outbox.js.map +1 -0
- package/packages/logging/dist/server/service.d.ts +83 -0
- package/packages/logging/dist/server/service.d.ts.map +1 -0
- package/packages/logging/dist/server/service.js +144 -0
- package/packages/logging/dist/server/service.js.map +1 -0
- package/packages/logging/dist/shipper.d.ts +20 -4
- package/packages/logging/dist/shipper.d.ts.map +1 -1
- package/packages/logging/dist/shipper.js +31 -5
- package/packages/logging/dist/shipper.js.map +1 -1
- package/packages/logging/dist/singleton.d.ts.map +1 -1
- package/packages/logging/dist/singleton.js +3 -0
- package/packages/logging/dist/singleton.js.map +1 -1
- package/packages/logging/dist/types.d.ts +28 -1
- package/packages/logging/dist/types.d.ts.map +1 -1
- package/packages/logging/dist/types.js +8 -0
- package/packages/logging/dist/types.js.map +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/logging/src/classification.ts +175 -0
- package/packages/logging/src/config.ts +60 -3
- package/packages/logging/src/index.ts +45 -1
- package/packages/logging/src/ingest/schema.ts +295 -0
- package/packages/logging/src/installation.ts +76 -0
- package/packages/logging/src/sensitive-keys.ts +40 -0
- package/packages/logging/src/server/credentials.ts +198 -0
- package/packages/logging/src/server/http.ts +170 -0
- package/packages/logging/src/server/index-store.ts +242 -0
- package/packages/logging/src/server/index.ts +56 -0
- package/packages/logging/src/server/keys.ts +32 -0
- package/packages/logging/src/server/outbox-worker.ts +124 -0
- package/packages/logging/src/server/outbox.ts +316 -0
- package/packages/logging/src/server/service.ts +196 -0
- package/packages/logging/src/shipper.ts +54 -8
- package/packages/logging/src/singleton.ts +3 -0
- package/packages/logging/src/types.ts +29 -1
- package/packages/logging/test/classification.test.ts +137 -0
- package/packages/logging/test/identity-transport.test.ts +197 -0
- package/packages/logging/test/ingest-schema.test.ts +184 -0
- package/packages/logging/test/integration.test.ts +9 -4
- package/packages/logging/test/log-http.test.ts +246 -0
- package/packages/logging/test/server.test.ts +444 -0
- package/packages/logging/test/shipper.test.ts +11 -11
- package/packages/logging/test/spool.test.ts +4 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/dist/adapters/detail-format.d.ts +34 -0
- package/packages/notification/dist/adapters/detail-format.d.ts.map +1 -0
- package/packages/notification/dist/adapters/detail-format.js +55 -0
- package/packages/notification/dist/adapters/detail-format.js.map +1 -0
- package/packages/notification/dist/adapters/email-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/email-adapter.js +3 -2
- package/packages/notification/dist/adapters/email-adapter.js.map +1 -1
- package/packages/notification/dist/adapters/line-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/line-adapter.js +3 -2
- package/packages/notification/dist/adapters/line-adapter.js.map +1 -1
- package/packages/notification/dist/adapters/ntfy-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/ntfy-adapter.js +3 -2
- package/packages/notification/dist/adapters/ntfy-adapter.js.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.d.ts +14 -0
- package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.js +83 -67
- package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +1 -0
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/dist/types.d.ts +3 -0
- package/packages/notification/dist/types.d.ts.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/adapters/detail-format.ts +74 -0
- package/packages/notification/src/adapters/email-adapter.ts +3 -2
- package/packages/notification/src/adapters/line-adapter.ts +3 -2
- package/packages/notification/src/adapters/ntfy-adapter.ts +3 -2
- package/packages/notification/src/adapters/telegram-adapter.ts +92 -74
- package/packages/notification/src/notification-center.ts +1 -0
- package/packages/notification/src/types.ts +3 -0
- package/packages/notification/test/detail-format.test.ts +106 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/unified-webhook-server.ts +86 -81
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata index for the log query service.
|
|
3
|
+
*
|
|
4
|
+
* R2 is an archive, not a search API, so the server keeps a small metadata
|
|
5
|
+
* index next to it: tenant, machine, session, timestamp, severity,
|
|
6
|
+
* source/event, batch id and the object key of the archived batch.
|
|
7
|
+
*
|
|
8
|
+
* The index deliberately holds NO message text. Prompts, responses and
|
|
9
|
+
* command output never enter it, and an agent asking "which errors happened
|
|
10
|
+
* in this session" gets metadata, not content.
|
|
11
|
+
*
|
|
12
|
+
* Storage is one NDJSON file per tenant and day:
|
|
13
|
+
*
|
|
14
|
+
* <dir>/<tenant>/2026-10-24.ndjson
|
|
15
|
+
*
|
|
16
|
+
* The index is tenant scoped by construction: every query is filtered by the
|
|
17
|
+
* tenant of the authenticated caller, and the tenant of the request body is
|
|
18
|
+
* never used.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import type { LogLevel } from "../types.js";
|
|
24
|
+
import { safeSegment, type OutboxBatch } from "./outbox.js";
|
|
25
|
+
|
|
26
|
+
export interface LogIndexEntry {
|
|
27
|
+
tenantId: string;
|
|
28
|
+
installationId: string;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
ts: string;
|
|
31
|
+
level: LogLevel;
|
|
32
|
+
source: string;
|
|
33
|
+
event: string;
|
|
34
|
+
batchId: string;
|
|
35
|
+
/** Object key of the archived batch, when it was uploaded. */
|
|
36
|
+
r2Key?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LogQueryParams {
|
|
40
|
+
/** Tenant of the authenticated caller. Required. */
|
|
41
|
+
tenantId: string;
|
|
42
|
+
installationId?: string;
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
/** ISO timestamp, inclusive lower bound. */
|
|
45
|
+
since?: string;
|
|
46
|
+
/** ISO timestamp, exclusive upper bound. */
|
|
47
|
+
until?: string;
|
|
48
|
+
levels?: LogLevel[];
|
|
49
|
+
source?: string;
|
|
50
|
+
event?: string;
|
|
51
|
+
/** Maximum number of entries. Default 100, hard maximum 500. */
|
|
52
|
+
limit?: number;
|
|
53
|
+
/** Opaque cursor of the previous page. */
|
|
54
|
+
cursor?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LogQueryResult {
|
|
58
|
+
entries: LogIndexEntry[];
|
|
59
|
+
/** Cursor of the next page, when there is one. */
|
|
60
|
+
nextCursor?: string;
|
|
61
|
+
hasMore: boolean;
|
|
62
|
+
/** Number of day files that were read. */
|
|
63
|
+
scannedFiles: number;
|
|
64
|
+
/** True when the scan hit its day budget. */
|
|
65
|
+
truncated: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LogIndexStoreOptions {
|
|
69
|
+
dir: string;
|
|
70
|
+
/** Days a query may scan back. Default 7, hard maximum 31. */
|
|
71
|
+
maxScanDays?: number;
|
|
72
|
+
/** Entries per page by default. */
|
|
73
|
+
defaultLimit?: number;
|
|
74
|
+
/** Hard maximum of entries per page. */
|
|
75
|
+
maxLimit?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toIsoDay(value: Date): string {
|
|
79
|
+
return value.toISOString().slice(0, 10);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function encodeCursor(value: string): string {
|
|
83
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function decodeCursor(value: string): string | undefined {
|
|
87
|
+
try {
|
|
88
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class LogIndexStore {
|
|
95
|
+
private readonly dir: string;
|
|
96
|
+
private readonly maxScanDays: number;
|
|
97
|
+
private readonly defaultLimit: number;
|
|
98
|
+
private readonly maxLimit: number;
|
|
99
|
+
|
|
100
|
+
constructor(options: LogIndexStoreOptions) {
|
|
101
|
+
this.dir = options.dir;
|
|
102
|
+
this.maxScanDays = Math.min(options.maxScanDays ?? 7, 31);
|
|
103
|
+
this.defaultLimit = options.defaultLimit ?? 100;
|
|
104
|
+
this.maxLimit = options.maxLimit ?? 500;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Index the metadata of a batch.
|
|
109
|
+
*
|
|
110
|
+
* Returns the number of entries that were written. The write is best
|
|
111
|
+
* effort: a failing index must not lose the batch itself.
|
|
112
|
+
*/
|
|
113
|
+
append(batch: OutboxBatch, r2Key?: string): number {
|
|
114
|
+
if (!Array.isArray(batch.records) || batch.records.length === 0) return 0;
|
|
115
|
+
|
|
116
|
+
const lines: string[] = [];
|
|
117
|
+
for (const record of batch.records) {
|
|
118
|
+
const entry: LogIndexEntry = {
|
|
119
|
+
tenantId: batch.tenantId,
|
|
120
|
+
installationId: batch.installationId,
|
|
121
|
+
ts: record.ts,
|
|
122
|
+
level: record.level,
|
|
123
|
+
source: record.source,
|
|
124
|
+
event: record.event,
|
|
125
|
+
batchId: batch.batchId,
|
|
126
|
+
};
|
|
127
|
+
if (batch.sessionId) entry.sessionId = batch.sessionId;
|
|
128
|
+
else if (record.sessionId) entry.sessionId = record.sessionId;
|
|
129
|
+
if (r2Key) entry.r2Key = r2Key;
|
|
130
|
+
lines.push(JSON.stringify(entry));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const day = toIsoDay(new Date(batch.sentAt));
|
|
135
|
+
const dir = join(this.dir, safeSegment(batch.tenantId));
|
|
136
|
+
mkdirSync(dir, { recursive: true });
|
|
137
|
+
appendFileSync(join(dir, `${day}.ndjson`), `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
138
|
+
return lines.length;
|
|
139
|
+
} catch {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Query the index for one tenant.
|
|
146
|
+
*
|
|
147
|
+
* The result is bounded by the page limit and by the day budget, and the
|
|
148
|
+
* caller is always a tenant: there is no way to query another tenant
|
|
149
|
+
* because the tenant comes from the parameter, which the server fills
|
|
150
|
+
* from the authenticated credential.
|
|
151
|
+
*/
|
|
152
|
+
query(params: LogQueryParams): LogQueryResult {
|
|
153
|
+
const limit = Math.min(params.limit ?? this.defaultLimit, this.maxLimit);
|
|
154
|
+
const days = this.dayRange(params);
|
|
155
|
+
const dir = join(this.dir, safeSegment(params.tenantId));
|
|
156
|
+
|
|
157
|
+
const matches: LogIndexEntry[] = [];
|
|
158
|
+
let scannedFiles = 0;
|
|
159
|
+
let truncated = days.truncated;
|
|
160
|
+
|
|
161
|
+
for (const day of days.days) {
|
|
162
|
+
const path = join(dir, `${day}.ndjson`);
|
|
163
|
+
if (!existsSync(path)) continue;
|
|
164
|
+
scannedFiles++;
|
|
165
|
+
let content: string;
|
|
166
|
+
try {
|
|
167
|
+
content = readFileSync(path, "utf8");
|
|
168
|
+
} catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
for (const line of content.split("\n")) {
|
|
172
|
+
if (!line.trim()) continue;
|
|
173
|
+
let entry: LogIndexEntry;
|
|
174
|
+
try {
|
|
175
|
+
entry = JSON.parse(line) as LogIndexEntry;
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!this.matches(entry, params)) continue;
|
|
180
|
+
matches.push(entry);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
matches.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
|
|
185
|
+
|
|
186
|
+
const cursor = params.cursor ? decodeCursor(params.cursor) : undefined;
|
|
187
|
+
let start = 0;
|
|
188
|
+
if (cursor) {
|
|
189
|
+
const index = matches.findIndex((entry) => `${entry.ts}|${entry.batchId}` === cursor);
|
|
190
|
+
start = index >= 0 ? index + 1 : 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const page = matches.slice(start, start + limit);
|
|
194
|
+
const hasMore = start + limit < matches.length;
|
|
195
|
+
const result: LogQueryResult = {
|
|
196
|
+
entries: page,
|
|
197
|
+
hasMore,
|
|
198
|
+
scannedFiles,
|
|
199
|
+
truncated,
|
|
200
|
+
};
|
|
201
|
+
if (hasMore && page.length > 0) {
|
|
202
|
+
const last = page[page.length - 1];
|
|
203
|
+
result.nextCursor = encodeCursor(`${last.ts}|${last.batchId}`);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private matches(entry: LogIndexEntry, params: LogQueryParams): boolean {
|
|
209
|
+
if (entry.tenantId !== params.tenantId) return false;
|
|
210
|
+
if (params.installationId && entry.installationId !== params.installationId) return false;
|
|
211
|
+
if (params.sessionId && entry.sessionId !== params.sessionId) return false;
|
|
212
|
+
if (params.levels && params.levels.length > 0 && !params.levels.includes(entry.level)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (params.source && entry.source !== params.source) return false;
|
|
216
|
+
if (params.event && entry.event !== params.event) return false;
|
|
217
|
+
if (params.since && entry.ts < params.since) return false;
|
|
218
|
+
if (params.until && entry.ts >= params.until) return false;
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private dayRange(params: LogQueryParams): { days: string[]; truncated: boolean } {
|
|
223
|
+
const now = new Date();
|
|
224
|
+
const until = params.until ? new Date(params.until) : now;
|
|
225
|
+
// The budget counts the scanned days, so the default window covers
|
|
226
|
+
// exactly `maxScanDays` days including the day of `until`.
|
|
227
|
+
const since = params.since
|
|
228
|
+
? new Date(params.since)
|
|
229
|
+
: new Date(until.getTime() - (this.maxScanDays - 1) * 86_400_000);
|
|
230
|
+
|
|
231
|
+
const days: string[] = [];
|
|
232
|
+
const cursor = new Date(since);
|
|
233
|
+
let truncated = false;
|
|
234
|
+
while (cursor <= until && days.length < this.maxScanDays) {
|
|
235
|
+
days.push(toIsoDay(cursor));
|
|
236
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
237
|
+
}
|
|
238
|
+
if (cursor <= until) truncated = true;
|
|
239
|
+
|
|
240
|
+
return { days, truncated };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server side of the logging pipeline.
|
|
3
|
+
*
|
|
4
|
+
* The server is the only component that writes to the archive: clients ship
|
|
5
|
+
* batches to the ingest endpoint, the server derives the tenant from the
|
|
6
|
+
* credential, queues the batch durably and a worker uploads it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_TENANT_ID,
|
|
11
|
+
LOG_READ_SCOPE,
|
|
12
|
+
LOG_WRITE_SCOPE,
|
|
13
|
+
LogCredentialStore,
|
|
14
|
+
hashToken,
|
|
15
|
+
readBearerToken,
|
|
16
|
+
} from "./credentials.js";
|
|
17
|
+
export type {
|
|
18
|
+
AuthenticatedCredential,
|
|
19
|
+
LogCredential,
|
|
20
|
+
LogCredentialStoreOptions,
|
|
21
|
+
} from "./credentials.js";
|
|
22
|
+
|
|
23
|
+
export { LogOutbox, outboxKey, safeSegment } from "./outbox.js";
|
|
24
|
+
export type {
|
|
25
|
+
EnqueueResult,
|
|
26
|
+
LogOutboxOptions,
|
|
27
|
+
OutboxBatch,
|
|
28
|
+
OutboxEntry,
|
|
29
|
+
OutboxStats,
|
|
30
|
+
} from "./outbox.js";
|
|
31
|
+
|
|
32
|
+
export { LogOutboxWorker } from "./outbox-worker.js";
|
|
33
|
+
export type { LogOutboxWorkerOptions, WorkerRunResult } from "./outbox-worker.js";
|
|
34
|
+
|
|
35
|
+
export { LogIndexStore } from "./index-store.js";
|
|
36
|
+
export type {
|
|
37
|
+
LogIndexEntry,
|
|
38
|
+
LogIndexStoreOptions,
|
|
39
|
+
LogQueryParams,
|
|
40
|
+
LogQueryResult,
|
|
41
|
+
} from "./index-store.js";
|
|
42
|
+
|
|
43
|
+
export { serverObjectKey } from "./keys.js";
|
|
44
|
+
export type { ObjectKeyInput } from "./keys.js";
|
|
45
|
+
|
|
46
|
+
export { LogIngestService } from "./service.js";
|
|
47
|
+
export type {
|
|
48
|
+
IngestFailure,
|
|
49
|
+
IngestResult,
|
|
50
|
+
IngestSuccess,
|
|
51
|
+
LogIngestServiceOptions,
|
|
52
|
+
QuerySuccess,
|
|
53
|
+
} from "./service.js";
|
|
54
|
+
|
|
55
|
+
export { createLogHttpHandler } from "./http.js";
|
|
56
|
+
export type { LogHttpHandler, LogHttpOptions } from "./http.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic object keys for the log archive.
|
|
3
|
+
*
|
|
4
|
+
* The server generates the keys - never the client - and they contain no
|
|
5
|
+
* user controlled path segment beyond an already sanitized tenant and batch
|
|
6
|
+
* id. The key is derived from the batch itself, so the same batch always
|
|
7
|
+
* lands under the same key and the metadata index can compute it without
|
|
8
|
+
* waiting for the upload.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { safeSegment } from "./outbox.js";
|
|
12
|
+
|
|
13
|
+
export interface ObjectKeyInput {
|
|
14
|
+
tenantId: string;
|
|
15
|
+
batchId: string;
|
|
16
|
+
/** ISO timestamp used for the date partition. */
|
|
17
|
+
sentAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the archive key of a batch:
|
|
22
|
+
*
|
|
23
|
+
* logs/<tenant>/2026/10/24/09/<batchId>.ndjson
|
|
24
|
+
*/
|
|
25
|
+
export function serverObjectKey(input: ObjectKeyInput): string {
|
|
26
|
+
const date = new Date(input.sentAt);
|
|
27
|
+
const year = date.getUTCFullYear();
|
|
28
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
29
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
30
|
+
const hour = String(date.getUTCHours()).padStart(2, "0");
|
|
31
|
+
return `logs/${safeSegment(input.tenantId)}/${year}/${month}/${day}/${hour}/${safeSegment(input.batchId)}.ndjson`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry worker for the server-side outbox.
|
|
3
|
+
*
|
|
4
|
+
* The worker drains the queue in the background, so an R2 outage never
|
|
5
|
+
* blocks the ingest endpoint: the batch waits in the queue and is retried
|
|
6
|
+
* with a bounded exponential backoff.
|
|
7
|
+
*
|
|
8
|
+
* A batch that exhausts its attempts stays in the queue (visible in the
|
|
9
|
+
* diagnostics) instead of being dropped silently.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { LogOutbox, OutboxEntry } from "./outbox.js";
|
|
13
|
+
|
|
14
|
+
export interface LogOutboxWorkerOptions {
|
|
15
|
+
outbox: LogOutbox;
|
|
16
|
+
/** Uploads one batch. Throwing marks the batch as failed. */
|
|
17
|
+
upload: (entry: OutboxEntry) => Promise<void>;
|
|
18
|
+
/** Interval between two drain runs. Default 5000 ms. */
|
|
19
|
+
intervalMs?: number;
|
|
20
|
+
/** Attempts before a batch is parked. Default 10. */
|
|
21
|
+
maxAttempts?: number;
|
|
22
|
+
/** First backoff step. Default 5000 ms. */
|
|
23
|
+
baseDelayMs?: number;
|
|
24
|
+
/** Upper bound of the backoff. Default 300000 ms (5 minutes). */
|
|
25
|
+
maxDelayMs?: number;
|
|
26
|
+
/** Batches per run. Default 50. */
|
|
27
|
+
batchLimit?: number;
|
|
28
|
+
/** Clock injection (tests). */
|
|
29
|
+
now?: () => number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WorkerRunResult {
|
|
33
|
+
uploaded: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
skipped: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class LogOutboxWorker {
|
|
39
|
+
private readonly outbox: LogOutbox;
|
|
40
|
+
private readonly upload: (entry: OutboxEntry) => Promise<void>;
|
|
41
|
+
private readonly intervalMs: number;
|
|
42
|
+
private readonly maxAttempts: number;
|
|
43
|
+
private readonly baseDelayMs: number;
|
|
44
|
+
private readonly maxDelayMs: number;
|
|
45
|
+
private readonly batchLimit: number;
|
|
46
|
+
private readonly now: () => number;
|
|
47
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
48
|
+
private running = false;
|
|
49
|
+
|
|
50
|
+
constructor(options: LogOutboxWorkerOptions) {
|
|
51
|
+
this.outbox = options.outbox;
|
|
52
|
+
this.upload = options.upload;
|
|
53
|
+
this.intervalMs = options.intervalMs ?? 5_000;
|
|
54
|
+
this.maxAttempts = options.maxAttempts ?? 10;
|
|
55
|
+
this.baseDelayMs = options.baseDelayMs ?? 5_000;
|
|
56
|
+
this.maxDelayMs = options.maxDelayMs ?? 300_000;
|
|
57
|
+
this.batchLimit = options.batchLimit ?? 50;
|
|
58
|
+
this.now = options.now ?? (() => Date.now());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Backoff for the given attempt count (1 = first failure). */
|
|
62
|
+
backoffMs(attempts: number): number {
|
|
63
|
+
const factor = 2 ** Math.max(0, attempts - 1);
|
|
64
|
+
return Math.min(this.baseDelayMs * factor, this.maxDelayMs);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Start the periodic drain. */
|
|
68
|
+
start(): void {
|
|
69
|
+
if (this.timer) return;
|
|
70
|
+
this.timer = setInterval(() => {
|
|
71
|
+
void this.runOnce();
|
|
72
|
+
}, this.intervalMs);
|
|
73
|
+
this.timer.unref?.();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Stop the periodic drain and wait for a running pass. */
|
|
77
|
+
async stop(): Promise<void> {
|
|
78
|
+
if (this.timer) {
|
|
79
|
+
clearInterval(this.timer);
|
|
80
|
+
this.timer = undefined;
|
|
81
|
+
}
|
|
82
|
+
while (this.running) {
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Drain the queue once. Never throws. */
|
|
88
|
+
async runOnce(): Promise<WorkerRunResult> {
|
|
89
|
+
if (this.running) return { uploaded: 0, failed: 0, skipped: 0 };
|
|
90
|
+
this.running = true;
|
|
91
|
+
const result: WorkerRunResult = { uploaded: 0, failed: 0, skipped: 0 };
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const now = this.now();
|
|
95
|
+
for (const entry of this.outbox.listPending(this.batchLimit)) {
|
|
96
|
+
if (entry.nextAttemptAt > now) {
|
|
97
|
+
result.skipped++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (entry.attempts >= this.maxAttempts) {
|
|
101
|
+
result.skipped++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await this.upload(entry);
|
|
107
|
+
this.outbox.markUploaded(entry);
|
|
108
|
+
result.uploaded++;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
111
|
+
const attempts = entry.attempts + 1;
|
|
112
|
+
this.outbox.markFailed(entry, message, this.now() + this.backoffMs(attempts));
|
|
113
|
+
result.failed++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// A failing drain must not kill the worker.
|
|
118
|
+
} finally {
|
|
119
|
+
this.running = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
}
|