openclaw-observability 2026.4.1 → 2026.4.21
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 +4 -4
- package/dist/cloud/api-key-auth.d.ts.map +1 -1
- package/dist/cloud/api-key-auth.js +4 -9
- package/dist/cloud/api-key-auth.js.map +1 -1
- package/dist/cloud/types.d.ts +2 -3
- package/dist/cloud/types.d.ts.map +1 -1
- package/dist/config.d.ts +34 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -2
- package/dist/config.js.map +1 -1
- package/dist/gateway/register-observability-gateway.d.ts +6 -4
- package/dist/gateway/register-observability-gateway.d.ts.map +1 -1
- package/dist/gateway/register-observability-gateway.js +105 -2
- package/dist/gateway/register-observability-gateway.js.map +1 -1
- package/dist/hooks/messages.d.ts +4 -3
- package/dist/hooks/messages.d.ts.map +1 -1
- package/dist/hooks/messages.js +23 -1
- package/dist/hooks/messages.js.map +1 -1
- package/dist/hooks/session.d.ts +4 -3
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +9 -4
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/subagent.d.ts +4 -3
- package/dist/hooks/subagent.d.ts.map +1 -1
- package/dist/hooks/subagent.js +4 -1
- package/dist/hooks/subagent.js.map +1 -1
- package/dist/hooks/tools.d.ts +3 -3
- package/dist/hooks/tools.d.ts.map +1 -1
- package/dist/hooks/tools.js +122 -4
- package/dist/hooks/tools.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +472 -118
- package/dist/index.js.map +1 -1
- package/dist/llm/replay-runtime.d.ts +16 -0
- package/dist/llm/replay-runtime.d.ts.map +1 -0
- package/dist/llm/replay-runtime.js +596 -0
- package/dist/llm/replay-runtime.js.map +1 -0
- package/dist/llm/replay.d.ts +3 -0
- package/dist/llm/replay.d.ts.map +1 -1
- package/dist/llm/replay.js.map +1 -1
- package/dist/redaction.d.ts +1 -1
- package/dist/redaction.js +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/session-context.d.ts +4 -3
- package/dist/runtime/session-context.d.ts.map +1 -1
- package/dist/runtime/session-context.js +37 -17
- package/dist/runtime/session-context.js.map +1 -1
- package/dist/security/chain-detector.d.ts +4 -4
- package/dist/security/chain-detector.d.ts.map +1 -1
- package/dist/security/chain-detector.js.map +1 -1
- package/dist/security/rules.d.ts +2 -2
- package/dist/security/rules.d.ts.map +1 -1
- package/dist/security/rules.js +9 -2
- package/dist/security/rules.js.map +1 -1
- package/dist/security/scanner.d.ts +8 -3
- package/dist/security/scanner.d.ts.map +1 -1
- package/dist/security/scanner.js +85 -7
- package/dist/security/scanner.js.map +1 -1
- package/dist/security/types.d.ts +3 -0
- package/dist/security/types.d.ts.map +1 -1
- package/dist/storage/buffer.d.ts +7 -7
- package/dist/storage/buffer.d.ts.map +1 -1
- package/dist/storage/buffer.js +2 -2
- package/dist/storage/buffer.js.map +1 -1
- package/dist/storage/cloud-export-writer.d.ts +23 -0
- package/dist/storage/cloud-export-writer.d.ts.map +1 -0
- package/dist/storage/cloud-export-writer.js +202 -0
- package/dist/storage/cloud-export-writer.js.map +1 -0
- package/dist/storage/duckdb-local-writer.d.ts +19 -3
- package/dist/storage/duckdb-local-writer.d.ts.map +1 -1
- package/dist/storage/duckdb-local-writer.js +261 -81
- package/dist/storage/duckdb-local-writer.js.map +1 -1
- package/dist/storage/duckdb-observability-forwarder.d.ts +16 -0
- package/dist/storage/duckdb-observability-forwarder.d.ts.map +1 -0
- package/dist/storage/duckdb-observability-forwarder.js +289 -0
- package/dist/storage/duckdb-observability-forwarder.js.map +1 -0
- package/dist/storage/mysql-writer.d.ts +35 -6
- package/dist/storage/mysql-writer.d.ts.map +1 -1
- package/dist/storage/mysql-writer.js +251 -32
- package/dist/storage/mysql-writer.js.map +1 -1
- package/dist/storage/schema.d.ts +2 -2
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +181 -53
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/structured-model.d.ts +11 -2
- package/dist/storage/structured-model.d.ts.map +1 -1
- package/dist/storage/structured-model.js +183 -5
- package/dist/storage/structured-model.js.map +1 -1
- package/dist/storage/writer.d.ts +14 -2
- package/dist/storage/writer.d.ts.map +1 -1
- package/dist/types.d.ts +28 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -1
- package/dist/types.js.map +1 -1
- package/dist/web/api.d.ts +80 -2
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +917 -113
- package/dist/web/api.js.map +1 -1
- package/dist/web/routes.d.ts +22 -2
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +264 -21
- package/dist/web/routes.js.map +1 -1
- package/dist/web/ui.d.ts +3 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +2678 -633
- package/dist/web/ui.js.map +1 -1
- package/openclaw.plugin.json +145 -4
- package/package.json +1 -1
|
@@ -38,9 +38,31 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.DuckDBLocalWriter = void 0;
|
|
41
|
+
exports.buildDuckDBOpenOptions = buildDuckDBOpenOptions;
|
|
41
42
|
const path = __importStar(require("path"));
|
|
42
43
|
const fs = __importStar(require("fs"));
|
|
43
44
|
const structured_model_1 = require("./structured-model");
|
|
45
|
+
const DEFAULT_DUCKDB_MEMORY_LIMIT = '1GB';
|
|
46
|
+
const DEFAULT_DUCKDB_THREADS = 2;
|
|
47
|
+
const MAX_DUCKDB_THREADS = 8;
|
|
48
|
+
const DEFAULT_DUCKDB_TEMP_DIRECTORY = path.join('/tmp', 'openclaw-observability-duckdb');
|
|
49
|
+
function buildDuckDBOpenOptions(config) {
|
|
50
|
+
const memoryLimit = typeof config.memoryLimit === 'string' && config.memoryLimit.trim()
|
|
51
|
+
? config.memoryLimit.trim()
|
|
52
|
+
: DEFAULT_DUCKDB_MEMORY_LIMIT;
|
|
53
|
+
const rawThreads = Number(config.threads ?? DEFAULT_DUCKDB_THREADS);
|
|
54
|
+
const threads = Number.isFinite(rawThreads)
|
|
55
|
+
? Math.min(MAX_DUCKDB_THREADS, Math.max(1, Math.floor(rawThreads)))
|
|
56
|
+
: DEFAULT_DUCKDB_THREADS;
|
|
57
|
+
const tempDirectory = typeof config.tempDirectory === 'string' && config.tempDirectory.trim()
|
|
58
|
+
? config.tempDirectory.trim()
|
|
59
|
+
: DEFAULT_DUCKDB_TEMP_DIRECTORY;
|
|
60
|
+
return {
|
|
61
|
+
memory_limit: memoryLimit,
|
|
62
|
+
threads: String(threads),
|
|
63
|
+
temp_directory: tempDirectory,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
44
66
|
/* ------------------------------------------------------------------ */
|
|
45
67
|
/* @duckdb/node-api Promise wrapper -> QueryPool adapter */
|
|
46
68
|
/* ------------------------------------------------------------------ */
|
|
@@ -252,9 +274,9 @@ class DuckDBPool {
|
|
|
252
274
|
*/
|
|
253
275
|
const DUCKDB_SCHEMA_STATEMENTS = [
|
|
254
276
|
// Drop legacy sequences that cause WAL write-write conflicts
|
|
255
|
-
`DROP SEQUENCE IF EXISTS
|
|
256
|
-
`DROP SEQUENCE IF EXISTS
|
|
257
|
-
`CREATE TABLE IF NOT EXISTS
|
|
277
|
+
`DROP SEQUENCE IF EXISTS observation_actions_id_seq`,
|
|
278
|
+
`DROP SEQUENCE IF EXISTS observation_alerts_id_seq`,
|
|
279
|
+
`CREATE TABLE IF NOT EXISTS observation_actions (
|
|
258
280
|
id BIGINT PRIMARY KEY,
|
|
259
281
|
session_id VARCHAR(64) NOT NULL,
|
|
260
282
|
action_type VARCHAR(32) NOT NULL,
|
|
@@ -266,18 +288,21 @@ const DUCKDB_SCHEMA_STATEMENTS = [
|
|
|
266
288
|
completion_tokens INTEGER DEFAULT NULL,
|
|
267
289
|
duration_ms INTEGER DEFAULT NULL,
|
|
268
290
|
user_id VARCHAR(128) DEFAULT '',
|
|
291
|
+
run_id VARCHAR(128) DEFAULT '',
|
|
292
|
+
parent_run_id VARCHAR(128) DEFAULT '',
|
|
269
293
|
channel_id VARCHAR(128) DEFAULT '',
|
|
270
294
|
created_at TIMESTAMP DEFAULT current_timestamp
|
|
271
295
|
)`,
|
|
272
|
-
`CREATE INDEX IF NOT EXISTS idx_act_session ON
|
|
273
|
-
`CREATE INDEX IF NOT EXISTS idx_act_type ON
|
|
274
|
-
`CREATE INDEX IF NOT EXISTS idx_act_created ON
|
|
275
|
-
`CREATE INDEX IF NOT EXISTS idx_act_user ON
|
|
296
|
+
`CREATE INDEX IF NOT EXISTS idx_act_session ON observation_actions(session_id)`,
|
|
297
|
+
`CREATE INDEX IF NOT EXISTS idx_act_type ON observation_actions(action_type)`,
|
|
298
|
+
`CREATE INDEX IF NOT EXISTS idx_act_created ON observation_actions(created_at)`,
|
|
299
|
+
`CREATE INDEX IF NOT EXISTS idx_act_user ON observation_actions(user_id)`,
|
|
276
300
|
// Note: idx_act_channel is created after migration (see migration logic below)
|
|
277
301
|
// This prevents errors when table exists without channel_id column
|
|
278
|
-
`CREATE TABLE IF NOT EXISTS
|
|
302
|
+
`CREATE TABLE IF NOT EXISTS observation_sessions (
|
|
279
303
|
session_id VARCHAR(64) PRIMARY KEY,
|
|
280
304
|
user_id VARCHAR(128) DEFAULT '',
|
|
305
|
+
parent_session_id VARCHAR(128) DEFAULT '',
|
|
281
306
|
model_name VARCHAR(128) DEFAULT '',
|
|
282
307
|
channel_id VARCHAR(128) DEFAULT '',
|
|
283
308
|
start_time TIMESTAMP NOT NULL,
|
|
@@ -285,11 +310,11 @@ const DUCKDB_SCHEMA_STATEMENTS = [
|
|
|
285
310
|
total_actions INTEGER DEFAULT 0,
|
|
286
311
|
total_tokens INTEGER DEFAULT 0
|
|
287
312
|
)`,
|
|
288
|
-
`CREATE INDEX IF NOT EXISTS idx_ses_user ON
|
|
289
|
-
`CREATE INDEX IF NOT EXISTS idx_ses_start ON
|
|
290
|
-
// Note: idx_ses_channel
|
|
313
|
+
`CREATE INDEX IF NOT EXISTS idx_ses_user ON observation_sessions(user_id)`,
|
|
314
|
+
`CREATE INDEX IF NOT EXISTS idx_ses_start ON observation_sessions(start_time)`,
|
|
315
|
+
// Note: idx_ses_parent / idx_ses_channel are created after migration
|
|
291
316
|
// This prevents errors when table exists without channel_id column
|
|
292
|
-
`CREATE TABLE IF NOT EXISTS
|
|
317
|
+
`CREATE TABLE IF NOT EXISTS observation_alerts (
|
|
293
318
|
id BIGINT PRIMARY KEY,
|
|
294
319
|
alert_id VARCHAR(64) NOT NULL,
|
|
295
320
|
session_id VARCHAR(64) NOT NULL,
|
|
@@ -308,12 +333,12 @@ const DUCKDB_SCHEMA_STATEMENTS = [
|
|
|
308
333
|
model_name VARCHAR(128) DEFAULT '',
|
|
309
334
|
created_at TIMESTAMP DEFAULT current_timestamp
|
|
310
335
|
)`,
|
|
311
|
-
`CREATE INDEX IF NOT EXISTS idx_alrt_sev ON
|
|
312
|
-
`CREATE INDEX IF NOT EXISTS idx_alrt_cat ON
|
|
313
|
-
`CREATE INDEX IF NOT EXISTS idx_alrt_sta ON
|
|
314
|
-
`CREATE INDEX IF NOT EXISTS idx_alrt_ses ON
|
|
315
|
-
`CREATE INDEX IF NOT EXISTS idx_alrt_cre ON
|
|
316
|
-
`CREATE INDEX IF NOT EXISTS idx_alrt_rul ON
|
|
336
|
+
`CREATE INDEX IF NOT EXISTS idx_alrt_sev ON observation_alerts(severity)`,
|
|
337
|
+
`CREATE INDEX IF NOT EXISTS idx_alrt_cat ON observation_alerts(category)`,
|
|
338
|
+
`CREATE INDEX IF NOT EXISTS idx_alrt_sta ON observation_alerts(status)`,
|
|
339
|
+
`CREATE INDEX IF NOT EXISTS idx_alrt_ses ON observation_alerts(session_id)`,
|
|
340
|
+
`CREATE INDEX IF NOT EXISTS idx_alrt_cre ON observation_alerts(created_at)`,
|
|
341
|
+
`CREATE INDEX IF NOT EXISTS idx_alrt_rul ON observation_alerts(rule_id)`,
|
|
317
342
|
`CREATE TABLE IF NOT EXISTS observability_metrics_samples (
|
|
318
343
|
id BIGINT PRIMARY KEY,
|
|
319
344
|
metric_name VARCHAR(256) NOT NULL,
|
|
@@ -330,6 +355,9 @@ const DUCKDB_SCHEMA_STATEMENTS = [
|
|
|
330
355
|
`CREATE INDEX IF NOT EXISTS idx_metrics_fp_ts ON observability_metrics_samples(label_fingerprint, sample_timestamp_ms)`,
|
|
331
356
|
`CREATE TABLE IF NOT EXISTS oc_traces (
|
|
332
357
|
trace_id VARCHAR(128) PRIMARY KEY,
|
|
358
|
+
organization_id VARCHAR(64) DEFAULT 'local',
|
|
359
|
+
scope_id VARCHAR(64) DEFAULT 'local',
|
|
360
|
+
environment VARCHAR(32) DEFAULT 'prod',
|
|
333
361
|
session_id VARCHAR(128) NOT NULL,
|
|
334
362
|
user_id VARCHAR(128) DEFAULT '',
|
|
335
363
|
channel_id VARCHAR(128) DEFAULT '',
|
|
@@ -347,6 +375,9 @@ const DUCKDB_SCHEMA_STATEMENTS = [
|
|
|
347
375
|
`CREATE INDEX IF NOT EXISTS idx_oc_trace_channel ON oc_traces(channel_id)`,
|
|
348
376
|
`CREATE TABLE IF NOT EXISTS oc_observations_core (
|
|
349
377
|
observation_id VARCHAR(160) PRIMARY KEY,
|
|
378
|
+
organization_id VARCHAR(64) DEFAULT 'local',
|
|
379
|
+
scope_id VARCHAR(64) DEFAULT 'local',
|
|
380
|
+
environment VARCHAR(32) DEFAULT 'prod',
|
|
350
381
|
trace_id VARCHAR(128) NOT NULL,
|
|
351
382
|
parent_observation_id VARCHAR(160),
|
|
352
383
|
root_observation_id VARCHAR(160) NOT NULL,
|
|
@@ -388,6 +419,9 @@ const DUCKDB_SCHEMA_STATEMENTS = [
|
|
|
388
419
|
)`,
|
|
389
420
|
`CREATE TABLE IF NOT EXISTS oc_events_raw (
|
|
390
421
|
event_id VARCHAR(180) PRIMARY KEY,
|
|
422
|
+
organization_id VARCHAR(64) DEFAULT 'local',
|
|
423
|
+
scope_id VARCHAR(64) DEFAULT 'local',
|
|
424
|
+
environment VARCHAR(32) DEFAULT 'prod',
|
|
391
425
|
trace_id VARCHAR(128) NOT NULL,
|
|
392
426
|
observation_id VARCHAR(160) NOT NULL,
|
|
393
427
|
event_type VARCHAR(64) NOT NULL,
|
|
@@ -448,6 +482,8 @@ const DUCKDB_SCHEMA_STATEMENTS = [
|
|
|
448
482
|
class DuckDBLocalWriter {
|
|
449
483
|
pool = null;
|
|
450
484
|
config;
|
|
485
|
+
dataRetentionDays;
|
|
486
|
+
startupMaintenanceDone = false;
|
|
451
487
|
initialized = false;
|
|
452
488
|
/** Promise for an in-progress initialization (allows concurrent callers to await) */
|
|
453
489
|
_initPromise = null;
|
|
@@ -461,8 +497,11 @@ class DuckDBLocalWriter {
|
|
|
461
497
|
* This gives ~10,000 unique IDs per millisecond.
|
|
462
498
|
*/
|
|
463
499
|
_idCounter = 0;
|
|
464
|
-
constructor(config) {
|
|
500
|
+
constructor(config, options) {
|
|
465
501
|
this.config = config;
|
|
502
|
+
this.dataRetentionDays = Number.isFinite(Number(options?.dataRetentionDays))
|
|
503
|
+
? Math.max(0, Math.floor(Number(options?.dataRetentionDays)))
|
|
504
|
+
: 30;
|
|
466
505
|
}
|
|
467
506
|
/** Generate a unique BIGINT ID without DuckDB SEQUENCE (precision-safe) */
|
|
468
507
|
_nextId() {
|
|
@@ -583,6 +622,10 @@ class DuckDBLocalWriter {
|
|
|
583
622
|
}
|
|
584
623
|
// Ensure parent directory exists
|
|
585
624
|
const dbPath = this.config.path;
|
|
625
|
+
const openOptions = buildDuckDBOpenOptions(this.config);
|
|
626
|
+
if (openOptions.temp_directory && !fs.existsSync(openOptions.temp_directory)) {
|
|
627
|
+
fs.mkdirSync(openOptions.temp_directory, { recursive: true });
|
|
628
|
+
}
|
|
586
629
|
if (dbPath !== ':memory:') {
|
|
587
630
|
const dir = path.dirname(dbPath);
|
|
588
631
|
if (!fs.existsSync(dir)) {
|
|
@@ -594,7 +637,7 @@ class DuckDBLocalWriter {
|
|
|
594
637
|
if (!fs.existsSync(dbPath)) {
|
|
595
638
|
// Create empty database file first (this creates the main .duckdb file)
|
|
596
639
|
// Then we'll open it normally for actual operations
|
|
597
|
-
const tempInstance = await duckdb.DuckDBInstance.create(dbPath);
|
|
640
|
+
const tempInstance = await duckdb.DuckDBInstance.create(dbPath, openOptions);
|
|
598
641
|
const tempConn = await tempInstance.connect();
|
|
599
642
|
// Immediately checkpoint to ensure main file is created (not just WAL)
|
|
600
643
|
// Use run() for DDL/commands (query() is for SELECT statements)
|
|
@@ -627,48 +670,60 @@ class DuckDBLocalWriter {
|
|
|
627
670
|
}
|
|
628
671
|
}
|
|
629
672
|
// Open database (main file now guaranteed to exist)
|
|
630
|
-
const instance = await duckdb.DuckDBInstance.create(dbPath);
|
|
673
|
+
const instance = await duckdb.DuckDBInstance.create(dbPath, openOptions);
|
|
631
674
|
const conn = await instance.connect();
|
|
632
675
|
this.pool = new DuckDBPool(instance, conn);
|
|
633
676
|
// Create table schema (execute one statement at a time)
|
|
634
677
|
for (const stmt of DUCKDB_SCHEMA_STATEMENTS) {
|
|
635
678
|
await this.pool.run(stmt);
|
|
636
679
|
}
|
|
637
|
-
// Migration:
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const hasChannelIdInSessions = await this._hasColumn('audit_sessions', 'channel_id');
|
|
653
|
-
if (!hasChannelIdInSessions) {
|
|
680
|
+
// Migration: channel_id for legacy DBs.
|
|
681
|
+
await this._ensureChannelIdColumn('observation_actions');
|
|
682
|
+
await this._ensureChannelIdColumn('observation_sessions');
|
|
683
|
+
// Migration: cloud-tenant columns for legacy local DuckDB databases.
|
|
684
|
+
// New query layer filters by scope_id; old databases created before this
|
|
685
|
+
// column existed would fail with Binder Error.
|
|
686
|
+
await this._ensureTenantColumn('observation_actions', 'organization_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
687
|
+
await this._ensureTenantColumn('observation_actions', 'scope_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
688
|
+
await this._ensureTenantColumn('observation_actions', 'environment', `VARCHAR(32) DEFAULT 'prod'`, 'prod');
|
|
689
|
+
await this._ensureActionRunLineageColumns();
|
|
690
|
+
await this._ensureTenantColumn('observation_sessions', 'organization_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
691
|
+
await this._ensureTenantColumn('observation_sessions', 'scope_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
692
|
+
await this._ensureTenantColumn('observation_sessions', 'environment', `VARCHAR(32) DEFAULT 'prod'`, 'prod');
|
|
693
|
+
await this._ensureColumn('observation_sessions', 'parent_session_id', `VARCHAR(128) DEFAULT ''`);
|
|
694
|
+
if (!(await this._hasIndex('observation_sessions', 'idx_ses_parent'))) {
|
|
654
695
|
try {
|
|
655
|
-
await this.pool.run(`
|
|
656
|
-
// Only log when actually adding the column (silent if already exists)
|
|
696
|
+
await this.pool.run(`CREATE INDEX idx_ses_parent ON observation_sessions(parent_session_id)`);
|
|
657
697
|
}
|
|
658
698
|
catch (err) {
|
|
659
699
|
const errMsg = err.message || String(err);
|
|
660
|
-
if (!errMsg.includes('already exists')
|
|
661
|
-
console.warn('[openclaw-observability] Failed to
|
|
700
|
+
if (!errMsg.includes('already exists')) {
|
|
701
|
+
console.warn('[openclaw-observability] Failed to create idx_ses_parent:', errMsg);
|
|
662
702
|
}
|
|
663
703
|
}
|
|
664
704
|
}
|
|
705
|
+
await this._ensureTenantColumn('observation_alerts', 'organization_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
706
|
+
await this._ensureTenantColumn('observation_alerts', 'scope_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
707
|
+
await this._ensureTenantColumn('observation_alerts', 'environment', `VARCHAR(32) DEFAULT 'prod'`, 'prod');
|
|
708
|
+
await this._ensureTenantColumn('observability_metrics_samples', 'organization_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
709
|
+
await this._ensureTenantColumn('observability_metrics_samples', 'scope_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
710
|
+
await this._ensureTenantColumn('observability_metrics_samples', 'environment', `VARCHAR(32) DEFAULT 'prod'`, 'prod');
|
|
711
|
+
await this._ensureTenantColumn('oc_traces', 'organization_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
712
|
+
await this._ensureTenantColumn('oc_traces', 'scope_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
713
|
+
await this._ensureTenantColumn('oc_traces', 'environment', `VARCHAR(32) DEFAULT 'prod'`, 'prod');
|
|
714
|
+
await this._ensureTenantColumn('oc_observations_core', 'organization_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
715
|
+
await this._ensureTenantColumn('oc_observations_core', 'scope_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
716
|
+
await this._ensureTenantColumn('oc_observations_core', 'environment', `VARCHAR(32) DEFAULT 'prod'`, 'prod');
|
|
717
|
+
await this._ensureTenantColumn('oc_events_raw', 'organization_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
718
|
+
await this._ensureTenantColumn('oc_events_raw', 'scope_id', `VARCHAR(64) DEFAULT 'local'`, 'local');
|
|
719
|
+
await this._ensureTenantColumn('oc_events_raw', 'environment', `VARCHAR(32) DEFAULT 'prod'`, 'prod');
|
|
665
720
|
// Create index if column exists and index doesn't exist (silent check)
|
|
666
|
-
const hasChannelIdColumn = await this._hasColumn('
|
|
721
|
+
const hasChannelIdColumn = await this._hasColumn('observation_actions', 'channel_id');
|
|
667
722
|
if (hasChannelIdColumn) {
|
|
668
|
-
const hasIndex = await this._hasIndex('
|
|
723
|
+
const hasIndex = await this._hasIndex('observation_actions', 'idx_act_channel');
|
|
669
724
|
if (!hasIndex) {
|
|
670
725
|
try {
|
|
671
|
-
await this.pool.run(`CREATE INDEX idx_act_channel ON
|
|
726
|
+
await this.pool.run(`CREATE INDEX idx_act_channel ON observation_actions(channel_id)`);
|
|
672
727
|
}
|
|
673
728
|
catch (err) {
|
|
674
729
|
const errMsg = err.message || String(err);
|
|
@@ -678,12 +733,12 @@ class DuckDBLocalWriter {
|
|
|
678
733
|
}
|
|
679
734
|
}
|
|
680
735
|
}
|
|
681
|
-
const hasChannelIdInSessionsCol = await this._hasColumn('
|
|
736
|
+
const hasChannelIdInSessionsCol = await this._hasColumn('observation_sessions', 'channel_id');
|
|
682
737
|
if (hasChannelIdInSessionsCol) {
|
|
683
|
-
const hasIndex = await this._hasIndex('
|
|
738
|
+
const hasIndex = await this._hasIndex('observation_sessions', 'idx_ses_channel');
|
|
684
739
|
if (!hasIndex) {
|
|
685
740
|
try {
|
|
686
|
-
await this.pool.run(`CREATE INDEX idx_ses_channel ON
|
|
741
|
+
await this.pool.run(`CREATE INDEX idx_ses_channel ON observation_sessions(channel_id)`);
|
|
687
742
|
}
|
|
688
743
|
catch (err) {
|
|
689
744
|
const errMsg = err.message || String(err);
|
|
@@ -712,6 +767,7 @@ class DuckDBLocalWriter {
|
|
|
712
767
|
// Still continue, but log the risk
|
|
713
768
|
}
|
|
714
769
|
await this._backfillStructuredFromLegacy();
|
|
770
|
+
await this.runStartupMaintenance();
|
|
715
771
|
this.initialized = true;
|
|
716
772
|
console.log(`[openclaw-observability] DuckDB writer initialized: ${dbPath}`);
|
|
717
773
|
}
|
|
@@ -742,12 +798,13 @@ class DuckDBLocalWriter {
|
|
|
742
798
|
if (!this.pool)
|
|
743
799
|
return;
|
|
744
800
|
// Ensure channel_id column exists before writing (migration safety check)
|
|
745
|
-
await this._ensureChannelIdColumn('
|
|
801
|
+
await this._ensureChannelIdColumn('observation_actions');
|
|
802
|
+
await this._ensureActionRunLineageColumns();
|
|
746
803
|
const sql = `
|
|
747
|
-
INSERT INTO
|
|
804
|
+
INSERT INTO observation_actions
|
|
748
805
|
(id, session_id, action_type, action_name, model_name, input_params, output_result,
|
|
749
|
-
prompt_tokens, completion_tokens, duration_ms, user_id, channel_id, created_at)
|
|
750
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
806
|
+
prompt_tokens, completion_tokens, duration_ms, user_id, run_id, parent_run_id, channel_id, created_at)
|
|
807
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
751
808
|
`;
|
|
752
809
|
for (const a of actions) {
|
|
753
810
|
await this.pool.run(sql, [
|
|
@@ -756,7 +813,7 @@ class DuckDBLocalWriter {
|
|
|
756
813
|
a.inputParams ? JSON.stringify(a.inputParams) : null,
|
|
757
814
|
a.outputResult ? JSON.stringify(a.outputResult) : null,
|
|
758
815
|
a.promptTokens, a.completionTokens, a.durationMs,
|
|
759
|
-
a.userId, a.channelId || '',
|
|
816
|
+
a.userId, a.runId || '', a.parentRunId || '', a.channelId || '',
|
|
760
817
|
this._formatDate(a.createdAt),
|
|
761
818
|
]);
|
|
762
819
|
}
|
|
@@ -769,13 +826,13 @@ class DuckDBLocalWriter {
|
|
|
769
826
|
for (const s of sessions)
|
|
770
827
|
deduped.set(s.sessionId, s);
|
|
771
828
|
for (const session of deduped.values()) {
|
|
772
|
-
// Aggregate stats from
|
|
829
|
+
// Aggregate stats from observation_actions in real-time
|
|
773
830
|
const [statsRows] = await this.pool.query(`SELECT
|
|
774
831
|
MIN(created_at) AS start_time,
|
|
775
832
|
MAX(created_at) AS end_time,
|
|
776
833
|
COUNT(*) AS total_actions,
|
|
777
834
|
COALESCE(SUM(COALESCE(prompt_tokens, 0) + COALESCE(completion_tokens, 0)), 0) AS total_tokens
|
|
778
|
-
FROM
|
|
835
|
+
FROM observation_actions WHERE session_id = ?`, [session.sessionId]);
|
|
779
836
|
const stats = statsRows[0];
|
|
780
837
|
const startTime = stats?.start_time || this._formatDate(session.startTime);
|
|
781
838
|
const endTime = stats?.end_time || this._formatDate(session.endTime);
|
|
@@ -784,19 +841,20 @@ class DuckDBLocalWriter {
|
|
|
784
841
|
// Use INSERT OR REPLACE to handle concurrent inserts atomically
|
|
785
842
|
// This prevents PRIMARY KEY constraint violations when multiple processes
|
|
786
843
|
// or concurrent transactions try to insert the same session_id
|
|
787
|
-
await this.pool.run(`INSERT INTO
|
|
788
|
-
(session_id, user_id, model_name, channel_id, start_time, end_time, total_actions, total_tokens)
|
|
789
|
-
VALUES (?, ?, ?, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP), ?, ?)
|
|
844
|
+
await this.pool.run(`INSERT INTO observation_sessions
|
|
845
|
+
(session_id, user_id, parent_session_id, model_name, channel_id, start_time, end_time, total_actions, total_tokens)
|
|
846
|
+
VALUES (?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP), ?, ?)
|
|
790
847
|
ON CONFLICT (session_id) DO UPDATE SET
|
|
791
|
-
user_id = COALESCE(EXCLUDED.user_id,
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
848
|
+
user_id = COALESCE(EXCLUDED.user_id, observation_sessions.user_id),
|
|
849
|
+
parent_session_id = CASE WHEN EXCLUDED.parent_session_id != '' THEN EXCLUDED.parent_session_id ELSE observation_sessions.parent_session_id END,
|
|
850
|
+
model_name = CASE WHEN EXCLUDED.model_name != '' THEN EXCLUDED.model_name ELSE observation_sessions.model_name END,
|
|
851
|
+
channel_id = CASE WHEN EXCLUDED.channel_id != '' THEN EXCLUDED.channel_id ELSE observation_sessions.channel_id END,
|
|
852
|
+
start_time = CASE WHEN observation_sessions.start_time < EXCLUDED.start_time THEN observation_sessions.start_time ELSE EXCLUDED.start_time END,
|
|
795
853
|
end_time = EXCLUDED.end_time,
|
|
796
854
|
total_actions = EXCLUDED.total_actions,
|
|
797
855
|
total_tokens = EXCLUDED.total_tokens`, [
|
|
798
856
|
session.sessionId,
|
|
799
|
-
session.userId, session.modelName, session.channelId || '',
|
|
857
|
+
session.userId, session.parentSessionId || '', session.modelName, session.channelId || '',
|
|
800
858
|
this._formatDate(session.startTime), this._formatDate(session.endTime),
|
|
801
859
|
totalActions, totalTokens,
|
|
802
860
|
]);
|
|
@@ -808,9 +866,12 @@ class DuckDBLocalWriter {
|
|
|
808
866
|
const batch = (0, structured_model_1.buildStructuredWriteBatch)(actions);
|
|
809
867
|
const traceSql = `
|
|
810
868
|
INSERT INTO oc_traces
|
|
811
|
-
(trace_id, session_id, user_id, channel_id, trace_name, status, tags_json, metadata_json, start_time, end_time, created_at, updated_at)
|
|
812
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP))
|
|
869
|
+
(trace_id, organization_id, scope_id, environment, session_id, user_id, channel_id, trace_name, status, tags_json, metadata_json, start_time, end_time, created_at, updated_at)
|
|
870
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP))
|
|
813
871
|
ON CONFLICT (trace_id) DO UPDATE SET
|
|
872
|
+
organization_id = CASE WHEN EXCLUDED.organization_id != '' THEN EXCLUDED.organization_id ELSE oc_traces.organization_id END,
|
|
873
|
+
scope_id = CASE WHEN EXCLUDED.scope_id != '' THEN EXCLUDED.scope_id ELSE oc_traces.scope_id END,
|
|
874
|
+
environment = CASE WHEN EXCLUDED.environment != '' THEN EXCLUDED.environment ELSE oc_traces.environment END,
|
|
814
875
|
user_id = CASE WHEN EXCLUDED.user_id != '' THEN EXCLUDED.user_id ELSE oc_traces.user_id END,
|
|
815
876
|
channel_id = CASE WHEN EXCLUDED.channel_id != '' THEN EXCLUDED.channel_id ELSE oc_traces.channel_id END,
|
|
816
877
|
trace_name = CASE WHEN EXCLUDED.trace_name != '' THEN EXCLUDED.trace_name ELSE oc_traces.trace_name END,
|
|
@@ -828,9 +889,12 @@ class DuckDBLocalWriter {
|
|
|
828
889
|
`;
|
|
829
890
|
const obsSql = `
|
|
830
891
|
INSERT INTO oc_observations_core
|
|
831
|
-
(observation_id, trace_id, parent_observation_id, root_observation_id, observation_type, observation_name, level, status, start_time, end_time, duration_ms, run_id, tool_call_id, model_name, provider, prompt_tokens, completion_tokens, total_tokens, cost_usd, user_id, session_id, channel_id, created_at, updated_at)
|
|
832
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP))
|
|
892
|
+
(observation_id, organization_id, scope_id, environment, trace_id, parent_observation_id, root_observation_id, observation_type, observation_name, level, status, start_time, end_time, duration_ms, run_id, tool_call_id, model_name, provider, prompt_tokens, completion_tokens, total_tokens, cost_usd, user_id, session_id, channel_id, created_at, updated_at)
|
|
893
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP))
|
|
833
894
|
ON CONFLICT (observation_id) DO UPDATE SET
|
|
895
|
+
organization_id = CASE WHEN EXCLUDED.organization_id != '' THEN EXCLUDED.organization_id ELSE oc_observations_core.organization_id END,
|
|
896
|
+
scope_id = CASE WHEN EXCLUDED.scope_id != '' THEN EXCLUDED.scope_id ELSE oc_observations_core.scope_id END,
|
|
897
|
+
environment = CASE WHEN EXCLUDED.environment != '' THEN EXCLUDED.environment ELSE oc_observations_core.environment END,
|
|
834
898
|
parent_observation_id = COALESCE(oc_observations_core.parent_observation_id, EXCLUDED.parent_observation_id),
|
|
835
899
|
root_observation_id = EXCLUDED.root_observation_id,
|
|
836
900
|
level = EXCLUDED.level,
|
|
@@ -871,13 +935,16 @@ class DuckDBLocalWriter {
|
|
|
871
935
|
`;
|
|
872
936
|
const eventSql = `
|
|
873
937
|
INSERT INTO oc_events_raw
|
|
874
|
-
(event_id, trace_id, observation_id, event_type, event_name, event_time, run_id, payload_json, created_at)
|
|
875
|
-
VALUES (?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), ?, ?, CAST(? AS TIMESTAMP))
|
|
938
|
+
(event_id, organization_id, scope_id, environment, trace_id, observation_id, event_type, event_name, event_time, run_id, payload_json, created_at)
|
|
939
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS TIMESTAMP), ?, ?, CAST(? AS TIMESTAMP))
|
|
876
940
|
ON CONFLICT (event_id) DO NOTHING
|
|
877
941
|
`;
|
|
878
942
|
for (const t of batch.traces) {
|
|
879
943
|
await this.pool.run(traceSql, [
|
|
880
944
|
t.traceId,
|
|
945
|
+
t.organizationId || 'local',
|
|
946
|
+
t.scopeId || 'local',
|
|
947
|
+
t.environment || 'prod',
|
|
881
948
|
t.sessionId,
|
|
882
949
|
t.userId,
|
|
883
950
|
t.channelId,
|
|
@@ -894,6 +961,9 @@ class DuckDBLocalWriter {
|
|
|
894
961
|
for (const o of batch.observations) {
|
|
895
962
|
await this.pool.run(obsSql, [
|
|
896
963
|
o.observationId,
|
|
964
|
+
o.organizationId || 'local',
|
|
965
|
+
o.scopeId || 'local',
|
|
966
|
+
o.environment || 'prod',
|
|
897
967
|
o.traceId,
|
|
898
968
|
o.parentObservationId,
|
|
899
969
|
o.rootObservationId,
|
|
@@ -934,6 +1004,9 @@ class DuckDBLocalWriter {
|
|
|
934
1004
|
for (const e of batch.events) {
|
|
935
1005
|
await this.pool.run(eventSql, [
|
|
936
1006
|
e.eventId,
|
|
1007
|
+
e.organizationId || 'local',
|
|
1008
|
+
e.scopeId || 'local',
|
|
1009
|
+
e.environment || 'prod',
|
|
937
1010
|
e.traceId,
|
|
938
1011
|
e.observationId,
|
|
939
1012
|
e.eventType,
|
|
@@ -951,7 +1024,7 @@ class DuckDBLocalWriter {
|
|
|
951
1024
|
try {
|
|
952
1025
|
const [legacyRows] = await this.pool.query(`SELECT session_id, action_type, action_name, model_name, input_params, output_result,
|
|
953
1026
|
prompt_tokens, completion_tokens, duration_ms, user_id, channel_id, created_at
|
|
954
|
-
FROM
|
|
1027
|
+
FROM observation_actions a
|
|
955
1028
|
WHERE NOT EXISTS (
|
|
956
1029
|
SELECT 1
|
|
957
1030
|
FROM oc_observations_core c
|
|
@@ -992,7 +1065,7 @@ class DuckDBLocalWriter {
|
|
|
992
1065
|
};
|
|
993
1066
|
});
|
|
994
1067
|
await this.writeStructured(actions);
|
|
995
|
-
console.log(`[openclaw-observability] Backfilled structured observations from legacy
|
|
1068
|
+
console.log(`[openclaw-observability] Backfilled structured observations from legacy observation_actions: ${actions.length}`);
|
|
996
1069
|
}
|
|
997
1070
|
catch (error) {
|
|
998
1071
|
console.warn('[openclaw-observability] Structured backfill skipped:', error.message);
|
|
@@ -1002,7 +1075,7 @@ class DuckDBLocalWriter {
|
|
|
1002
1075
|
if (!this.pool || alerts.length === 0)
|
|
1003
1076
|
return;
|
|
1004
1077
|
const sql = `
|
|
1005
|
-
INSERT INTO
|
|
1078
|
+
INSERT INTO observation_alerts
|
|
1006
1079
|
(id, alert_id, session_id, action_type, action_name, rule_id, rule_name,
|
|
1007
1080
|
category, severity, finding, context, status, user_id, model_name, created_at)
|
|
1008
1081
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1140,6 +1213,7 @@ class DuckDBLocalWriter {
|
|
|
1140
1213
|
await this.pool.close();
|
|
1141
1214
|
this.pool = null;
|
|
1142
1215
|
this.initialized = false;
|
|
1216
|
+
this.startupMaintenanceDone = false;
|
|
1143
1217
|
console.log('[openclaw-observability] DuckDB writer closed (checkpoint done)');
|
|
1144
1218
|
}
|
|
1145
1219
|
}
|
|
@@ -1202,23 +1276,98 @@ class DuckDBLocalWriter {
|
|
|
1202
1276
|
async _ensureChannelIdColumn(tableName) {
|
|
1203
1277
|
if (!this.pool)
|
|
1204
1278
|
return;
|
|
1205
|
-
const exists = await this._hasColumn(tableName, 'channel_id');
|
|
1206
|
-
if (exists)
|
|
1207
|
-
return; // Column exists, no action needed
|
|
1208
1279
|
try {
|
|
1209
|
-
|
|
1210
|
-
|
|
1280
|
+
// Do not rely on _hasColumn() for old DB variants; make DDL idempotent.
|
|
1281
|
+
await this.pool.run(`ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS channel_id VARCHAR(128) DEFAULT ''`);
|
|
1211
1282
|
}
|
|
1212
1283
|
catch (err) {
|
|
1213
1284
|
const errMsg = err.message || String(err);
|
|
1214
|
-
// If column already exists, that's fine (silent success)
|
|
1215
|
-
if (errMsg.includes('already exists') || errMsg.includes('duplicate column')) {
|
|
1216
|
-
return; // Column exists, no action needed
|
|
1217
|
-
}
|
|
1218
1285
|
// Other errors should be logged
|
|
1219
1286
|
console.warn(`[openclaw-observability] Failed to ensure channel_id in ${tableName}:`, errMsg);
|
|
1220
1287
|
}
|
|
1221
1288
|
}
|
|
1289
|
+
async _ensureActionRunLineageColumns() {
|
|
1290
|
+
if (!this.pool)
|
|
1291
|
+
return;
|
|
1292
|
+
try {
|
|
1293
|
+
await this.pool.run(`ALTER TABLE observation_actions ADD COLUMN IF NOT EXISTS run_id VARCHAR(128) DEFAULT ''`);
|
|
1294
|
+
await this.pool.run(`ALTER TABLE observation_actions ADD COLUMN IF NOT EXISTS parent_run_id VARCHAR(128) DEFAULT ''`);
|
|
1295
|
+
await this.pool.run(`UPDATE observation_actions
|
|
1296
|
+
SET run_id = ''
|
|
1297
|
+
WHERE run_id IS NULL`);
|
|
1298
|
+
await this.pool.run(`UPDATE observation_actions
|
|
1299
|
+
SET parent_run_id = ''
|
|
1300
|
+
WHERE parent_run_id IS NULL`);
|
|
1301
|
+
}
|
|
1302
|
+
catch (err) {
|
|
1303
|
+
const errMsg = err.message || String(err);
|
|
1304
|
+
console.warn('[openclaw-observability] Failed to ensure run lineage columns:', errMsg);
|
|
1305
|
+
}
|
|
1306
|
+
if (!(await this._hasIndex('observation_actions', 'idx_act_run_id'))) {
|
|
1307
|
+
try {
|
|
1308
|
+
await this.pool.run(`CREATE INDEX idx_act_run_id ON observation_actions(run_id)`);
|
|
1309
|
+
}
|
|
1310
|
+
catch (err) {
|
|
1311
|
+
const errMsg = err.message || String(err);
|
|
1312
|
+
if (!errMsg.includes('already exists')) {
|
|
1313
|
+
console.warn('[openclaw-observability] Failed to create idx_act_run_id:', errMsg);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (!(await this._hasIndex('observation_actions', 'idx_act_parent_run_id'))) {
|
|
1318
|
+
try {
|
|
1319
|
+
await this.pool.run(`CREATE INDEX idx_act_parent_run_id ON observation_actions(parent_run_id)`);
|
|
1320
|
+
}
|
|
1321
|
+
catch (err) {
|
|
1322
|
+
const errMsg = err.message || String(err);
|
|
1323
|
+
if (!errMsg.includes('already exists')) {
|
|
1324
|
+
console.warn('[openclaw-observability] Failed to create idx_act_parent_run_id:', errMsg);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
async _ensureColumn(tableName, columnName, columnDef) {
|
|
1330
|
+
if (!this.pool)
|
|
1331
|
+
return;
|
|
1332
|
+
try {
|
|
1333
|
+
// Do not rely on _hasColumn() for old DB variants; make DDL idempotent.
|
|
1334
|
+
await this.pool.run(`ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS ${columnName} ${columnDef}`);
|
|
1335
|
+
}
|
|
1336
|
+
catch (err) {
|
|
1337
|
+
const errMsg = err.message || String(err);
|
|
1338
|
+
if (!errMsg.includes('already exists') && !errMsg.includes('duplicate column')) {
|
|
1339
|
+
console.warn(`[openclaw-observability] Failed to add ${columnName} to ${tableName}:`, errMsg);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Ensure a tenant-related column exists and legacy rows are backfilled.
|
|
1345
|
+
* This keeps old local DuckDB files compatible with new project-scoped queries.
|
|
1346
|
+
*/
|
|
1347
|
+
async _ensureTenantColumn(tableName, columnName, columnDef, defaultValue) {
|
|
1348
|
+
if (!this.pool)
|
|
1349
|
+
return;
|
|
1350
|
+
try {
|
|
1351
|
+
// Do not rely on _hasColumn() for old DB variants; make DDL idempotent.
|
|
1352
|
+
await this.pool.run(`ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS ${columnName} ${columnDef}`);
|
|
1353
|
+
}
|
|
1354
|
+
catch (err) {
|
|
1355
|
+
const errMsg = err.message || String(err);
|
|
1356
|
+
if (!errMsg.includes('already exists') && !errMsg.includes('duplicate column')) {
|
|
1357
|
+
console.warn(`[openclaw-observability] Failed to add ${columnName} to ${tableName}:`, errMsg);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
try {
|
|
1362
|
+
await this.pool.run(`UPDATE ${tableName}
|
|
1363
|
+
SET ${columnName} = ?
|
|
1364
|
+
WHERE ${columnName} IS NULL OR CAST(${columnName} AS VARCHAR) = ''`, [defaultValue]);
|
|
1365
|
+
}
|
|
1366
|
+
catch (err) {
|
|
1367
|
+
const errMsg = err.message || String(err);
|
|
1368
|
+
console.warn(`[openclaw-observability] Failed to backfill ${columnName} in ${tableName}:`, errMsg);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1222
1371
|
_formatDate(d) {
|
|
1223
1372
|
if (!d)
|
|
1224
1373
|
return this._formatTimestamp(new Date());
|
|
@@ -1233,6 +1382,37 @@ class DuckDBLocalWriter {
|
|
|
1233
1382
|
return this._formatTimestamp(d);
|
|
1234
1383
|
return String(d);
|
|
1235
1384
|
}
|
|
1385
|
+
async runStartupMaintenance() {
|
|
1386
|
+
if (this.startupMaintenanceDone)
|
|
1387
|
+
return;
|
|
1388
|
+
await this._cleanupExpiredDataOnStartup();
|
|
1389
|
+
this.startupMaintenanceDone = true;
|
|
1390
|
+
}
|
|
1391
|
+
async _cleanupExpiredDataOnStartup() {
|
|
1392
|
+
if (!this.pool || this.dataRetentionDays <= 0)
|
|
1393
|
+
return;
|
|
1394
|
+
const cutoffDate = new Date(Date.now() - this.dataRetentionDays * 24 * 60 * 60 * 1000);
|
|
1395
|
+
const cutoffText = this._formatDate(cutoffDate);
|
|
1396
|
+
const cutoffMs = cutoffDate.getTime();
|
|
1397
|
+
await this.pool.run('DELETE FROM observation_alerts WHERE created_at < ?', [cutoffText]);
|
|
1398
|
+
await this.pool.run('DELETE FROM observation_actions WHERE created_at < ?', [cutoffText]);
|
|
1399
|
+
await this.pool.run('DELETE FROM observation_sessions WHERE COALESCE(end_time, start_time) < ?', [cutoffText]);
|
|
1400
|
+
await this.pool.run('DELETE FROM oc_events_raw WHERE event_time < ?', [cutoffText]);
|
|
1401
|
+
await this.pool.run(`DELETE FROM oc_observations_payload
|
|
1402
|
+
WHERE observation_id IN (
|
|
1403
|
+
SELECT observation_id
|
|
1404
|
+
FROM oc_observations_core
|
|
1405
|
+
WHERE COALESCE(end_time, start_time, created_at) < ?
|
|
1406
|
+
)`, [cutoffText]);
|
|
1407
|
+
await this.pool.run('DELETE FROM oc_observations_core WHERE COALESCE(end_time, start_time, created_at) < ?', [cutoffText]);
|
|
1408
|
+
await this.pool.run('DELETE FROM oc_traces WHERE COALESCE(end_time, start_time, created_at) < ?', [cutoffText]);
|
|
1409
|
+
await this.pool.run('DELETE FROM observability_metrics_samples WHERE sample_timestamp_ms < ?', [cutoffMs]);
|
|
1410
|
+
await this.pool.run('DELETE FROM oc_metric_points_number WHERE ts_ms < ?', [cutoffMs]);
|
|
1411
|
+
await this.pool.run('DELETE FROM oc_metric_points_histogram WHERE ts_ms < ?', [cutoffMs]);
|
|
1412
|
+
await this.pool.run('DELETE FROM oc_metric_series WHERE last_seen_ms < ?', [cutoffMs]);
|
|
1413
|
+
await this.pool.run('DELETE FROM oc_observations_payload WHERE observation_id NOT IN (SELECT observation_id FROM oc_observations_core)');
|
|
1414
|
+
console.log(`[openclaw-observability] Startup data retention cleanup completed for DuckDB (days=${this.dataRetentionDays}, cutoff=${cutoffDate.toISOString()})`);
|
|
1415
|
+
}
|
|
1236
1416
|
_formatTimestamp(d) {
|
|
1237
1417
|
return d.toISOString().replace('T', ' ').replace('Z', '');
|
|
1238
1418
|
}
|