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
package/dist/web/api.js
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Observation Web API — query database and return structured data
|
|
4
4
|
* Compatible with both MySQL and DuckDB backends
|
|
5
5
|
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
6
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.getTenants = getTenants;
|
|
41
|
+
exports.getSkillsOverview = getSkillsOverview;
|
|
42
|
+
exports.getSkillDetail = getSkillDetail;
|
|
7
43
|
exports.getStats = getStats;
|
|
8
44
|
exports.getSessions = getSessions;
|
|
9
45
|
exports.getSessionActions = getSessionActions;
|
|
@@ -16,6 +52,12 @@ exports.getAnalytics = getAnalytics;
|
|
|
16
52
|
exports.getMetricsOverview = getMetricsOverview;
|
|
17
53
|
exports.getMetricSeries = getMetricSeries;
|
|
18
54
|
exports.getTraceObservationTree = getTraceObservationTree;
|
|
55
|
+
const fs = __importStar(require("node:fs"));
|
|
56
|
+
const os = __importStar(require("node:os"));
|
|
57
|
+
const path = __importStar(require("node:path"));
|
|
58
|
+
const REPLAY_SESSION_PREFIX = 'replay-';
|
|
59
|
+
const SKILL_CATALOG_CACHE_TTL_MS = 60_000;
|
|
60
|
+
const skillCatalogCache = new Map();
|
|
19
61
|
/* ------------------------------------------------------------------ */
|
|
20
62
|
/* API functions */
|
|
21
63
|
/* ------------------------------------------------------------------ */
|
|
@@ -39,6 +81,564 @@ function parseTimeMs(value) {
|
|
|
39
81
|
const ms = Date.parse(trimmed);
|
|
40
82
|
return Number.isFinite(ms) ? ms : undefined;
|
|
41
83
|
}
|
|
84
|
+
function normalizeScopeId(value) {
|
|
85
|
+
const v = (value || '').trim();
|
|
86
|
+
return v || 'local';
|
|
87
|
+
}
|
|
88
|
+
function safeParseJsonObject(value) {
|
|
89
|
+
if (typeof value !== 'string' || !value.trim())
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(value);
|
|
93
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// ignore invalid json
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function decodeXmlEntities(input) {
|
|
103
|
+
return input
|
|
104
|
+
.replace(/"/g, '"')
|
|
105
|
+
.replace(/'/g, '\'')
|
|
106
|
+
.replace(/</g, '<')
|
|
107
|
+
.replace(/>/g, '>')
|
|
108
|
+
.replace(/&/g, '&');
|
|
109
|
+
}
|
|
110
|
+
function readSkillCatalogFromSystemPrompt(systemPrompt) {
|
|
111
|
+
if (!systemPrompt || typeof systemPrompt !== 'string')
|
|
112
|
+
return [];
|
|
113
|
+
const out = [];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
const xmlRe = /<skill>\s*<name>([\s\S]*?)<\/name>[\s\S]*?<description>([\s\S]*?)<\/description>[\s\S]*?<location>([\s\S]*?)<\/location>[\s\S]*?<\/skill>/gi;
|
|
116
|
+
let m;
|
|
117
|
+
while ((m = xmlRe.exec(systemPrompt)) !== null) {
|
|
118
|
+
const skill = decodeXmlEntities(String(m[1] || '')).trim();
|
|
119
|
+
if (!skill || seen.has(skill))
|
|
120
|
+
continue;
|
|
121
|
+
seen.add(skill);
|
|
122
|
+
const description = decodeXmlEntities(String(m[2] || '')).trim();
|
|
123
|
+
const location = decodeXmlEntities(String(m[3] || '')).trim();
|
|
124
|
+
out.push({
|
|
125
|
+
skill,
|
|
126
|
+
description: description || undefined,
|
|
127
|
+
location: location || undefined,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function extractSystemPrompt(inputObj) {
|
|
133
|
+
const sp = typeof inputObj.systemPrompt === 'string'
|
|
134
|
+
? inputObj.systemPrompt
|
|
135
|
+
: (typeof inputObj.extraSystemPrompt === 'string' ? inputObj.extraSystemPrompt : '');
|
|
136
|
+
return String(sp || '');
|
|
137
|
+
}
|
|
138
|
+
function parseSkillCallInput(input) {
|
|
139
|
+
const obj = safeParseJsonObject(input);
|
|
140
|
+
if (!obj)
|
|
141
|
+
return null;
|
|
142
|
+
const skill = typeof obj.skill === 'string' ? obj.skill.trim() : '';
|
|
143
|
+
if (!skill)
|
|
144
|
+
return null;
|
|
145
|
+
const toolCallId = typeof obj.toolCallId === 'string' ? obj.toolCallId.trim() : '';
|
|
146
|
+
const skillPath = typeof obj.skillPath === 'string' ? obj.skillPath.trim() : '';
|
|
147
|
+
const skillSource = typeof obj.skillSource === 'string' ? obj.skillSource.trim() : '';
|
|
148
|
+
return {
|
|
149
|
+
skill,
|
|
150
|
+
toolCallId: toolCallId || undefined,
|
|
151
|
+
skillPath: skillPath || undefined,
|
|
152
|
+
skillSource: skillSource || undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function resolveSkillLocation(rawLocation) {
|
|
156
|
+
const value = String(rawLocation || '').trim();
|
|
157
|
+
if (!value)
|
|
158
|
+
return '';
|
|
159
|
+
if (value === '~')
|
|
160
|
+
return os.homedir();
|
|
161
|
+
if (value.startsWith('~/'))
|
|
162
|
+
return path.join(os.homedir(), value.slice(2));
|
|
163
|
+
if (value.startsWith('$HOME/'))
|
|
164
|
+
return path.join(os.homedir(), value.slice(6));
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
function inferSkillSourceFromLocation(rawLocation) {
|
|
168
|
+
const resolved = resolveSkillLocation(rawLocation);
|
|
169
|
+
const normalized = String(resolved || '').replace(/\\/g, '/');
|
|
170
|
+
if (!normalized)
|
|
171
|
+
return '';
|
|
172
|
+
const home = os.homedir().replace(/\\/g, '/');
|
|
173
|
+
const stateDir = (String(process.env.OPENCLAW_STATE_DIR || '').trim() || path.join(home, '.openclaw'))
|
|
174
|
+
.replace(/\\/g, '/');
|
|
175
|
+
const bundledOverride = String(process.env.OPENCLAW_BUNDLED_SKILLS_DIR || '').trim().replace(/\\/g, '/');
|
|
176
|
+
const bundledSibling = path.join(path.dirname(process.execPath || ''), 'skills').replace(/\\/g, '/');
|
|
177
|
+
if (normalized.includes('/node_modules/openclaw/skills/'))
|
|
178
|
+
return 'openclaw-bundled';
|
|
179
|
+
if (bundledOverride && normalized.startsWith(`${bundledOverride}/`))
|
|
180
|
+
return 'openclaw-bundled';
|
|
181
|
+
if (bundledSibling && normalized.startsWith(`${bundledSibling}/`))
|
|
182
|
+
return 'openclaw-bundled';
|
|
183
|
+
if (normalized.startsWith(`${stateDir}/skills/`))
|
|
184
|
+
return 'openclaw-managed';
|
|
185
|
+
if (normalized.startsWith(`${stateDir}/workspace/skills/`))
|
|
186
|
+
return 'openclaw-workspace';
|
|
187
|
+
if (normalized.startsWith(`${home}/.agents/skills/`))
|
|
188
|
+
return 'agents-skills-personal';
|
|
189
|
+
if (normalized.includes('/.agents/skills/'))
|
|
190
|
+
return 'agents-skills-project';
|
|
191
|
+
if (normalized.includes('/workspace/skills/'))
|
|
192
|
+
return 'openclaw-workspace';
|
|
193
|
+
return 'openclaw-extra';
|
|
194
|
+
}
|
|
195
|
+
function getSkillSearchDirs() {
|
|
196
|
+
const dirs = [];
|
|
197
|
+
const home = os.homedir();
|
|
198
|
+
const stateDir = String(process.env.OPENCLAW_STATE_DIR || '').trim() || path.join(home, '.openclaw');
|
|
199
|
+
const envHomes = [
|
|
200
|
+
process.env.OPENCLAW_HOME,
|
|
201
|
+
process.env.OPENCLAW_STATE_DIR,
|
|
202
|
+
].map((v) => String(v || '').trim()).filter(Boolean);
|
|
203
|
+
const bundledOverride = String(process.env.OPENCLAW_BUNDLED_SKILLS_DIR || '').trim();
|
|
204
|
+
const bundledSibling = path.join(path.dirname(process.execPath || ''), 'skills');
|
|
205
|
+
const nodeInstallRoot = path.dirname(path.dirname(process.execPath || ''));
|
|
206
|
+
const nodeGlobalOpenClawSkills = path.join(nodeInstallRoot, 'lib', 'node_modules', 'openclaw', 'skills');
|
|
207
|
+
dirs.push(path.join(stateDir, 'skills'));
|
|
208
|
+
envHomes.forEach((h) => dirs.push(path.join(h, 'workspace', 'skills')));
|
|
209
|
+
dirs.push(path.join(stateDir, 'workspace', 'skills'));
|
|
210
|
+
dirs.push(path.join(home, '.agents', 'skills'));
|
|
211
|
+
dirs.push(path.join(process.cwd(), '.agents', 'skills'));
|
|
212
|
+
dirs.push(path.join(process.cwd(), 'skills'));
|
|
213
|
+
dirs.push(path.join(home, '.openclaw', 'workspace', 'skills'));
|
|
214
|
+
dirs.push(path.join(process.cwd(), 'workspace', 'skills'));
|
|
215
|
+
dirs.push(nodeGlobalOpenClawSkills);
|
|
216
|
+
if (bundledOverride)
|
|
217
|
+
dirs.push(bundledOverride);
|
|
218
|
+
if (bundledSibling)
|
|
219
|
+
dirs.push(bundledSibling);
|
|
220
|
+
// de-dup while preserving order
|
|
221
|
+
return Array.from(new Set(dirs.map((d) => path.resolve(d))));
|
|
222
|
+
}
|
|
223
|
+
function inferSkillDescriptionFromMarkdown(content) {
|
|
224
|
+
const text = String(content || '');
|
|
225
|
+
const lines = text.split(/\r?\n/).map((x) => x.trim());
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
if (!line)
|
|
228
|
+
continue;
|
|
229
|
+
if (line.startsWith('#'))
|
|
230
|
+
continue;
|
|
231
|
+
if (line.startsWith('```'))
|
|
232
|
+
continue;
|
|
233
|
+
return line.length > 220 ? `${line.slice(0, 217)}...` : line;
|
|
234
|
+
}
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
237
|
+
function discoverLocalSkills() {
|
|
238
|
+
const out = new Map();
|
|
239
|
+
const roots = getSkillSearchDirs();
|
|
240
|
+
for (const root of roots) {
|
|
241
|
+
let entries = [];
|
|
242
|
+
try {
|
|
243
|
+
if (!fs.existsSync(root))
|
|
244
|
+
continue;
|
|
245
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
for (const ent of entries) {
|
|
251
|
+
if (!ent.isDirectory())
|
|
252
|
+
continue;
|
|
253
|
+
const skill = String(ent.name || '').trim();
|
|
254
|
+
if (!skill || out.has(skill))
|
|
255
|
+
continue;
|
|
256
|
+
const skillFile = path.join(root, skill, 'SKILL.md');
|
|
257
|
+
if (!fs.existsSync(skillFile))
|
|
258
|
+
continue;
|
|
259
|
+
let description = '';
|
|
260
|
+
try {
|
|
261
|
+
description = inferSkillDescriptionFromMarkdown(fs.readFileSync(skillFile, 'utf8'));
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
description = '';
|
|
265
|
+
}
|
|
266
|
+
out.set(skill, {
|
|
267
|
+
description: description || undefined,
|
|
268
|
+
location: skillFile,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
function normalizeAgentScope(value) {
|
|
275
|
+
const raw = String(value || 'main').trim().toLowerCase();
|
|
276
|
+
if (raw === 'subagent' || raw === 'replay' || raw === 'system' || raw === 'all')
|
|
277
|
+
return raw;
|
|
278
|
+
return 'main';
|
|
279
|
+
}
|
|
280
|
+
function applyAgentScopeSessionFilter(where, values, scopeId, agentScope) {
|
|
281
|
+
const subagentPromptClause = `session_id IN (
|
|
282
|
+
SELECT DISTINCT session_id FROM observation_actions
|
|
283
|
+
WHERE scope_id = ?
|
|
284
|
+
AND action_type IN ('prompt_build', 'message')
|
|
285
|
+
AND (
|
|
286
|
+
input_params LIKE '%[Subagent Context]%'
|
|
287
|
+
OR input_params LIKE '%"source":"subagent"%'
|
|
288
|
+
)
|
|
289
|
+
)`;
|
|
290
|
+
const llmSessionClause = `session_id IN (
|
|
291
|
+
SELECT DISTINCT session_id FROM observation_actions
|
|
292
|
+
WHERE scope_id = ?
|
|
293
|
+
AND action_type = 'message'
|
|
294
|
+
AND action_name LIKE 'llm_call:%'
|
|
295
|
+
)`;
|
|
296
|
+
const replaySessionClause = `session_id IN (
|
|
297
|
+
SELECT DISTINCT session_id FROM observation_actions
|
|
298
|
+
WHERE scope_id = ?
|
|
299
|
+
AND action_type = 'replay'
|
|
300
|
+
)`;
|
|
301
|
+
const systemSessionClause = `(
|
|
302
|
+
LOWER(COALESCE(user_id,'')) = 'system'
|
|
303
|
+
OR LOWER(COALESCE(session_id,'')) = 'system'
|
|
304
|
+
OR LOWER(COALESCE(channel_id,'')) LIKE '%heartbeat%'
|
|
305
|
+
OR LOWER(COALESCE(user_id,'')) LIKE '%heartbeat%'
|
|
306
|
+
OR LOWER(COALESCE(session_id,'')) LIKE '%heartbeat%'
|
|
307
|
+
)`;
|
|
308
|
+
if (agentScope === 'replay') {
|
|
309
|
+
where += ' AND session_id LIKE ?';
|
|
310
|
+
values.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
311
|
+
where += ` AND ${replaySessionClause}`;
|
|
312
|
+
values.push(scopeId);
|
|
313
|
+
return { where, values };
|
|
314
|
+
}
|
|
315
|
+
where += ' AND session_id NOT LIKE ?';
|
|
316
|
+
values.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
317
|
+
if (agentScope === 'system') {
|
|
318
|
+
where += ` AND ${systemSessionClause}`;
|
|
319
|
+
return { where, values };
|
|
320
|
+
}
|
|
321
|
+
where += " AND LOWER(COALESCE(user_id,'')) NOT IN ('unknown','unkown')";
|
|
322
|
+
where += " AND LOWER(COALESCE(session_id,'')) NOT IN ('unknown','unkown','-')";
|
|
323
|
+
if (agentScope === 'all') {
|
|
324
|
+
where += ` AND (${systemSessionClause} OR ${llmSessionClause})`;
|
|
325
|
+
values.push(scopeId);
|
|
326
|
+
return { where, values };
|
|
327
|
+
}
|
|
328
|
+
where += ` AND ${llmSessionClause}`;
|
|
329
|
+
values.push(scopeId);
|
|
330
|
+
if (agentScope === 'subagent') {
|
|
331
|
+
where += ` AND (COALESCE(TRIM(parent_session_id), '') <> '' OR ${subagentPromptClause})`;
|
|
332
|
+
values.push(scopeId);
|
|
333
|
+
return { where, values };
|
|
334
|
+
}
|
|
335
|
+
where += ` AND NOT (${systemSessionClause})`;
|
|
336
|
+
where += ` AND (COALESCE(TRIM(parent_session_id), '') = '' AND NOT (${subagentPromptClause}))`;
|
|
337
|
+
values.push(scopeId);
|
|
338
|
+
return { where, values };
|
|
339
|
+
}
|
|
340
|
+
/** List available scope ids */
|
|
341
|
+
async function getTenants(pool) {
|
|
342
|
+
const [rows] = await pool.query(`SELECT
|
|
343
|
+
COALESCE(NULLIF(TRIM(scope_id), ''), 'local') AS scope_id
|
|
344
|
+
FROM observation_sessions
|
|
345
|
+
WHERE session_id NOT LIKE ?
|
|
346
|
+
GROUP BY 1
|
|
347
|
+
ORDER BY CASE WHEN LOWER(COALESCE(NULLIF(TRIM(scope_id), ''), 'local')) = 'local' THEN 0 ELSE 1 END, scope_id ASC`, [`${REPLAY_SESSION_PREFIX}%`]);
|
|
348
|
+
return { tenants: rows };
|
|
349
|
+
}
|
|
350
|
+
async function getSkillCatalog(pool, scopeId) {
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
const hit = skillCatalogCache.get(scopeId);
|
|
353
|
+
if (hit && (now - hit.cachedAt) < SKILL_CATALOG_CACHE_TTL_MS) {
|
|
354
|
+
return new Map(hit.catalog);
|
|
355
|
+
}
|
|
356
|
+
const baseSql = `SELECT input_params
|
|
357
|
+
FROM observation_actions
|
|
358
|
+
WHERE scope_id = ?
|
|
359
|
+
AND action_type IN ('message', 'replay')
|
|
360
|
+
AND action_name LIKE 'llm_call:%'
|
|
361
|
+
AND input_params IS NOT NULL
|
|
362
|
+
ORDER BY created_at DESC
|
|
363
|
+
LIMIT ?`;
|
|
364
|
+
const [hotRows] = await pool.query(baseSql, [scopeId, 8]);
|
|
365
|
+
let rows = hotRows;
|
|
366
|
+
if (!rows.length) {
|
|
367
|
+
const [allRows] = await pool.query(baseSql, [scopeId, 80]);
|
|
368
|
+
rows = allRows;
|
|
369
|
+
}
|
|
370
|
+
const catalog = new Map();
|
|
371
|
+
const seenPromptHashes = new Set();
|
|
372
|
+
let staleRounds = 0;
|
|
373
|
+
for (const row of rows) {
|
|
374
|
+
const inputObj = safeParseJsonObject(row.input_params);
|
|
375
|
+
if (!inputObj)
|
|
376
|
+
continue;
|
|
377
|
+
const promptHash = typeof inputObj.systemPromptHash === 'string'
|
|
378
|
+
? inputObj.systemPromptHash.trim()
|
|
379
|
+
: '';
|
|
380
|
+
if (promptHash && seenPromptHashes.has(promptHash))
|
|
381
|
+
continue;
|
|
382
|
+
if (promptHash)
|
|
383
|
+
seenPromptHashes.add(promptHash);
|
|
384
|
+
const systemPrompt = extractSystemPrompt(inputObj);
|
|
385
|
+
if (!systemPrompt)
|
|
386
|
+
continue;
|
|
387
|
+
const before = catalog.size;
|
|
388
|
+
const items = readSkillCatalogFromSystemPrompt(systemPrompt);
|
|
389
|
+
for (const item of items) {
|
|
390
|
+
if (!catalog.has(item.skill)) {
|
|
391
|
+
catalog.set(item.skill, {
|
|
392
|
+
description: item.description,
|
|
393
|
+
location: item.location,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (catalog.size === before) {
|
|
398
|
+
staleRounds += 1;
|
|
399
|
+
if (catalog.size > 0 && staleRounds >= 8)
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
staleRounds = 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!catalog.size && rows.length < 20) {
|
|
407
|
+
const [fallbackRows] = await pool.query(baseSql, [scopeId, 80]);
|
|
408
|
+
for (const row of fallbackRows) {
|
|
409
|
+
const inputObj = safeParseJsonObject(row.input_params);
|
|
410
|
+
if (!inputObj)
|
|
411
|
+
continue;
|
|
412
|
+
const promptHash = typeof inputObj.systemPromptHash === 'string'
|
|
413
|
+
? inputObj.systemPromptHash.trim()
|
|
414
|
+
: '';
|
|
415
|
+
if (promptHash && seenPromptHashes.has(promptHash))
|
|
416
|
+
continue;
|
|
417
|
+
if (promptHash)
|
|
418
|
+
seenPromptHashes.add(promptHash);
|
|
419
|
+
const systemPrompt = extractSystemPrompt(inputObj);
|
|
420
|
+
if (!systemPrompt)
|
|
421
|
+
continue;
|
|
422
|
+
const items = readSkillCatalogFromSystemPrompt(systemPrompt);
|
|
423
|
+
for (const item of items) {
|
|
424
|
+
if (!catalog.has(item.skill)) {
|
|
425
|
+
catalog.set(item.skill, {
|
|
426
|
+
description: item.description,
|
|
427
|
+
location: item.location,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (catalog.size > 0)
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Keep local file-system discovery scoped to local tenant only.
|
|
436
|
+
if (scopeId === 'local') {
|
|
437
|
+
const localSkills = discoverLocalSkills();
|
|
438
|
+
for (const [skill, meta] of localSkills.entries()) {
|
|
439
|
+
if (!catalog.has(skill)) {
|
|
440
|
+
catalog.set(skill, meta);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
const existing = catalog.get(skill) || {};
|
|
444
|
+
catalog.set(skill, {
|
|
445
|
+
description: existing.description || meta.description,
|
|
446
|
+
location: existing.location || meta.location,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
skillCatalogCache.set(scopeId, { cachedAt: now, catalog: new Map(catalog) });
|
|
452
|
+
return catalog;
|
|
453
|
+
}
|
|
454
|
+
/** Skills overview aggregated from realtime skill_call events */
|
|
455
|
+
async function getSkillsOverview(pool, params) {
|
|
456
|
+
const scopeId = normalizeScopeId(params?.scopeId);
|
|
457
|
+
const limit = Math.max(1, Math.min(500, normalizePositiveInt(params?.limit, 200)));
|
|
458
|
+
const fromMs = parseTimeMs(params?.timeFrom);
|
|
459
|
+
const toMs = parseTimeMs(params?.timeTo);
|
|
460
|
+
let where = `scope_id = ? AND action_type = 'skill_call'`;
|
|
461
|
+
const values = [scopeId];
|
|
462
|
+
if (fromMs != null) {
|
|
463
|
+
where += ' AND created_at >= FROM_UNIXTIME(? / 1000)';
|
|
464
|
+
values.push(fromMs);
|
|
465
|
+
}
|
|
466
|
+
if (toMs != null) {
|
|
467
|
+
where += ' AND created_at <= FROM_UNIXTIME(? / 1000)';
|
|
468
|
+
values.push(toMs);
|
|
469
|
+
}
|
|
470
|
+
const [rows] = await pool.query(`SELECT session_id, run_id, action_name, input_params, created_at
|
|
471
|
+
FROM observation_actions
|
|
472
|
+
WHERE ${where}
|
|
473
|
+
ORDER BY created_at DESC
|
|
474
|
+
LIMIT 20000`, values);
|
|
475
|
+
const perSkill = new Map();
|
|
476
|
+
for (const row of rows) {
|
|
477
|
+
const parsed = parseSkillCallInput(row.input_params);
|
|
478
|
+
const fromName = String(row.action_name || '').startsWith('skill_call:')
|
|
479
|
+
? String(row.action_name || '').slice('skill_call:'.length).trim()
|
|
480
|
+
: '';
|
|
481
|
+
const skillName = parsed?.skill || fromName;
|
|
482
|
+
if (!skillName)
|
|
483
|
+
continue;
|
|
484
|
+
const sessionId = String(row.session_id || '').trim();
|
|
485
|
+
const runId = String(row.run_id || '').trim();
|
|
486
|
+
const ts = Date.parse(String(row.created_at || '')) || 0;
|
|
487
|
+
let rec = perSkill.get(skillName);
|
|
488
|
+
if (!rec) {
|
|
489
|
+
rec = { callCount: 0, sessions: new Set(), runs: new Set(), lastSeen: 0, sourceCounts: new Map() };
|
|
490
|
+
perSkill.set(skillName, rec);
|
|
491
|
+
}
|
|
492
|
+
rec.callCount += 1;
|
|
493
|
+
if (sessionId)
|
|
494
|
+
rec.sessions.add(sessionId);
|
|
495
|
+
if (runId)
|
|
496
|
+
rec.runs.add(runId);
|
|
497
|
+
if (ts > rec.lastSeen)
|
|
498
|
+
rec.lastSeen = ts;
|
|
499
|
+
if (parsed?.skillSource) {
|
|
500
|
+
rec.sourceCounts.set(parsed.skillSource, Number(rec.sourceCounts.get(parsed.skillSource) || 0) + 1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const catalog = await getSkillCatalog(pool, scopeId);
|
|
504
|
+
for (const skill of catalog.keys()) {
|
|
505
|
+
if (!perSkill.has(skill)) {
|
|
506
|
+
perSkill.set(skill, { callCount: 0, sessions: new Set(), runs: new Set(), lastSeen: 0, sourceCounts: new Map() });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const items = Array.from(perSkill.entries())
|
|
510
|
+
.map(([skill, rec]) => {
|
|
511
|
+
const cat = catalog.get(skill);
|
|
512
|
+
let source = '';
|
|
513
|
+
let sourceCount = -1;
|
|
514
|
+
rec.sourceCounts.forEach((cnt, name) => {
|
|
515
|
+
if (cnt > sourceCount) {
|
|
516
|
+
source = name;
|
|
517
|
+
sourceCount = cnt;
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
if (!source) {
|
|
521
|
+
source = inferSkillSourceFromLocation(cat?.location || '');
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
skill,
|
|
525
|
+
description: cat?.description,
|
|
526
|
+
location: cat?.location,
|
|
527
|
+
source: source || undefined,
|
|
528
|
+
callCount: rec.callCount,
|
|
529
|
+
sessionCount: rec.sessions.size,
|
|
530
|
+
runCount: rec.runs.size,
|
|
531
|
+
lastSeen: rec.lastSeen > 0 ? new Date(rec.lastSeen).toISOString() : '',
|
|
532
|
+
};
|
|
533
|
+
})
|
|
534
|
+
.sort((a, b) => (b.callCount - a.callCount) || a.skill.localeCompare(b.skill))
|
|
535
|
+
.slice(0, limit);
|
|
536
|
+
return {
|
|
537
|
+
totalSkills: perSkill.size,
|
|
538
|
+
totalCalls: Array.from(perSkill.values()).reduce((sum, it) => sum + it.callCount, 0),
|
|
539
|
+
items,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
async function getSkillDetail(pool, params) {
|
|
543
|
+
const scopeId = normalizeScopeId(params.scopeId);
|
|
544
|
+
const skill = String(params.skill || '').trim();
|
|
545
|
+
if (!skill) {
|
|
546
|
+
throw new Error('skill is required');
|
|
547
|
+
}
|
|
548
|
+
const limit = Math.max(1, Math.min(200, normalizePositiveInt(params.limit, 50)));
|
|
549
|
+
const fromMs = parseTimeMs(params.timeFrom);
|
|
550
|
+
const toMs = parseTimeMs(params.timeTo);
|
|
551
|
+
let where = `scope_id = ? AND action_type = 'skill_call' AND action_name = ?`;
|
|
552
|
+
const values = [scopeId, `skill_call:${skill}`];
|
|
553
|
+
if (fromMs != null) {
|
|
554
|
+
where += ' AND created_at >= FROM_UNIXTIME(? / 1000)';
|
|
555
|
+
values.push(fromMs);
|
|
556
|
+
}
|
|
557
|
+
if (toMs != null) {
|
|
558
|
+
where += ' AND created_at <= FROM_UNIXTIME(? / 1000)';
|
|
559
|
+
values.push(toMs);
|
|
560
|
+
}
|
|
561
|
+
const [rows] = await pool.query(`SELECT session_id, run_id, input_params, output_result, created_at
|
|
562
|
+
FROM observation_actions
|
|
563
|
+
WHERE ${where}
|
|
564
|
+
ORDER BY created_at DESC
|
|
565
|
+
LIMIT 20000`, values);
|
|
566
|
+
const sessions = new Set();
|
|
567
|
+
const runs = new Set();
|
|
568
|
+
let callCount = 0;
|
|
569
|
+
let lastSeenMs = 0;
|
|
570
|
+
const recentCalls = [];
|
|
571
|
+
const seenToolCallIds = new Set();
|
|
572
|
+
let latestPath = '';
|
|
573
|
+
let latestSource = '';
|
|
574
|
+
for (const row of rows) {
|
|
575
|
+
callCount += 1;
|
|
576
|
+
const sessionId = String(row.session_id || '').trim();
|
|
577
|
+
const runId = String(row.run_id || '').trim();
|
|
578
|
+
if (sessionId)
|
|
579
|
+
sessions.add(sessionId);
|
|
580
|
+
if (runId)
|
|
581
|
+
runs.add(runId);
|
|
582
|
+
const ts = Date.parse(String(row.created_at || '')) || 0;
|
|
583
|
+
if (ts > lastSeenMs)
|
|
584
|
+
lastSeenMs = ts;
|
|
585
|
+
const inp = parseSkillCallInput(row.input_params);
|
|
586
|
+
const out = safeParseJsonObject(row.output_result);
|
|
587
|
+
const ok = typeof out?.ok === 'boolean' ? out.ok : undefined;
|
|
588
|
+
const error = typeof out?.error === 'string' ? out.error : undefined;
|
|
589
|
+
if (inp?.skillPath && !latestPath)
|
|
590
|
+
latestPath = inp.skillPath;
|
|
591
|
+
if (inp?.skillSource && !latestSource)
|
|
592
|
+
latestSource = inp.skillSource;
|
|
593
|
+
if (recentCalls.length < limit) {
|
|
594
|
+
const dedupeKey = `${sessionId}|${runId}|${inp?.toolCallId || ''}|${String(row.created_at || '')}`;
|
|
595
|
+
if (!seenToolCallIds.has(dedupeKey)) {
|
|
596
|
+
seenToolCallIds.add(dedupeKey);
|
|
597
|
+
recentCalls.push({
|
|
598
|
+
sessionId,
|
|
599
|
+
runId,
|
|
600
|
+
createdAt: String(row.created_at || ''),
|
|
601
|
+
toolCallId: inp?.toolCallId,
|
|
602
|
+
skillPath: inp?.skillPath,
|
|
603
|
+
skillSource: inp?.skillSource,
|
|
604
|
+
ok,
|
|
605
|
+
error,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const catalog = await getSkillCatalog(pool, scopeId);
|
|
611
|
+
const cat = catalog.get(skill);
|
|
612
|
+
const locationRaw = cat?.location || latestPath || '';
|
|
613
|
+
const inferredSource = inferSkillSourceFromLocation(locationRaw);
|
|
614
|
+
const location = resolveSkillLocation(locationRaw);
|
|
615
|
+
let content = '';
|
|
616
|
+
let contentLoaded = false;
|
|
617
|
+
if (location) {
|
|
618
|
+
try {
|
|
619
|
+
if (fs.existsSync(location)) {
|
|
620
|
+
content = fs.readFileSync(location, 'utf8');
|
|
621
|
+
contentLoaded = true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
contentLoaded = false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
skill,
|
|
630
|
+
description: cat?.description,
|
|
631
|
+
location: locationRaw || undefined,
|
|
632
|
+
source: latestSource || inferredSource || undefined,
|
|
633
|
+
content,
|
|
634
|
+
contentLoaded,
|
|
635
|
+
callCount,
|
|
636
|
+
sessionCount: sessions.size,
|
|
637
|
+
runCount: runs.size,
|
|
638
|
+
lastSeen: lastSeenMs > 0 ? new Date(lastSeenMs).toISOString() : '',
|
|
639
|
+
recentCalls,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
42
642
|
function resolveMetricsWindow(params) {
|
|
43
643
|
const now = Date.now();
|
|
44
644
|
const defaultMinutes = Math.min(Math.max(normalizePositiveInt(params.minutes, 60), 1), 24 * 60);
|
|
@@ -60,8 +660,11 @@ function resolveMetricsWindow(params) {
|
|
|
60
660
|
}
|
|
61
661
|
/** Get summary statistics */
|
|
62
662
|
async function getStats(pool, params) {
|
|
63
|
-
|
|
64
|
-
const
|
|
663
|
+
const scopeId = normalizeScopeId(params?.scopeId);
|
|
664
|
+
const agentScope = normalizeAgentScope(params?.agentType);
|
|
665
|
+
let sessionWhere = 'scope_id = ?';
|
|
666
|
+
let sessionVals = [scopeId];
|
|
667
|
+
({ where: sessionWhere, values: sessionVals } = applyAgentScopeSessionFilter(sessionWhere, sessionVals, scopeId, agentScope));
|
|
65
668
|
if (params?.timeFrom) {
|
|
66
669
|
sessionWhere += ' AND COALESCE(end_time, start_time) >= ?';
|
|
67
670
|
sessionVals.push(params.timeFrom);
|
|
@@ -70,8 +673,9 @@ async function getStats(pool, params) {
|
|
|
70
673
|
sessionWhere += ' AND COALESCE(end_time, start_time) <= ?';
|
|
71
674
|
sessionVals.push(params.timeTo);
|
|
72
675
|
}
|
|
73
|
-
let actionWhere = '
|
|
74
|
-
const actionVals = [];
|
|
676
|
+
let actionWhere = 'scope_id = ? AND session_id IN (SELECT session_id FROM observation_sessions WHERE ' + sessionWhere + ')';
|
|
677
|
+
const actionVals = [scopeId];
|
|
678
|
+
actionVals.push(...sessionVals);
|
|
75
679
|
if (params?.timeFrom) {
|
|
76
680
|
actionWhere += ' AND created_at >= ?';
|
|
77
681
|
actionVals.push(params.timeFrom);
|
|
@@ -80,16 +684,16 @@ async function getStats(pool, params) {
|
|
|
80
684
|
actionWhere += ' AND created_at <= ?';
|
|
81
685
|
actionVals.push(params.timeTo);
|
|
82
686
|
}
|
|
83
|
-
const [sessionRows] = await pool.query('SELECT COUNT(*) as cnt FROM
|
|
687
|
+
const [sessionRows] = await pool.query('SELECT COUNT(*) as cnt FROM observation_sessions WHERE ' + sessionWhere, sessionVals);
|
|
84
688
|
const [actionRows] = await pool.query(`SELECT COUNT(*) as cnt,
|
|
85
689
|
COALESCE(SUM(COALESCE(prompt_tokens,0) + COALESCE(completion_tokens,0)), 0) as tokens,
|
|
86
690
|
COALESCE(AVG(NULLIF(duration_ms, 0)), 0) as avg_latency,
|
|
87
691
|
COALESCE(SUM(CASE WHEN action_type = 'agent_end' THEN 1 ELSE 0 END), 0) as total_end,
|
|
88
692
|
COALESCE(SUM(CASE WHEN action_type = 'agent_end' AND output_result LIKE '%"success":true%' THEN 1 ELSE 0 END), 0) as success_end
|
|
89
|
-
FROM
|
|
693
|
+
FROM observation_actions
|
|
90
694
|
WHERE ` + actionWhere, actionVals);
|
|
91
695
|
// Count by action_type
|
|
92
|
-
const [typeCounts] = await pool.query('SELECT action_type, COUNT(*) as cnt FROM
|
|
696
|
+
const [typeCounts] = await pool.query('SELECT action_type, COUNT(*) as cnt FROM observation_actions WHERE ' + actionWhere + ' GROUP BY action_type ORDER BY cnt DESC', actionVals);
|
|
93
697
|
const actionTypeCounts = {};
|
|
94
698
|
for (const row of typeCounts) {
|
|
95
699
|
actionTypeCounts[row.action_type] = Number(row.cnt);
|
|
@@ -109,11 +713,14 @@ async function getStats(pool, params) {
|
|
|
109
713
|
}
|
|
110
714
|
/** Get session list */
|
|
111
715
|
async function getSessions(pool, params) {
|
|
716
|
+
const scopeId = normalizeScopeId(params.scopeId);
|
|
112
717
|
const page = normalizePositiveInt(params.page, 1);
|
|
113
718
|
const limit = Math.min(normalizePositiveInt(params.limit, 20), 100);
|
|
114
719
|
const offset = (page - 1) * limit;
|
|
115
|
-
|
|
116
|
-
|
|
720
|
+
const agentScope = normalizeAgentScope(params.agentType);
|
|
721
|
+
let where = 'scope_id = ?';
|
|
722
|
+
let values = [scopeId];
|
|
723
|
+
({ where, values } = applyAgentScopeSessionFilter(where, values, scopeId, agentScope));
|
|
117
724
|
if (params.sessionId) {
|
|
118
725
|
where += ' AND session_id = ?';
|
|
119
726
|
values.push(params.sessionId);
|
|
@@ -132,11 +739,11 @@ async function getSessions(pool, params) {
|
|
|
132
739
|
where += ` AND (
|
|
133
740
|
session_id LIKE ? OR user_id LIKE ? OR model_name LIKE ?
|
|
134
741
|
OR session_id IN (
|
|
135
|
-
SELECT DISTINCT session_id FROM
|
|
136
|
-
WHERE input_params LIKE ? OR output_result LIKE ? OR action_name LIKE ?
|
|
742
|
+
SELECT DISTINCT session_id FROM observation_actions
|
|
743
|
+
WHERE scope_id = ? AND (input_params LIKE ? OR output_result LIKE ? OR action_name LIKE ?)
|
|
137
744
|
)
|
|
138
745
|
)`;
|
|
139
|
-
values.push(like, like, like, like, like, like);
|
|
746
|
+
values.push(like, like, like, scopeId, like, like, like);
|
|
140
747
|
}
|
|
141
748
|
// Time range filter: use "last active time" (end_time fallback start_time)
|
|
142
749
|
// so "recent 24h" reflects recently updated sessions, not only newly started sessions.
|
|
@@ -148,23 +755,49 @@ async function getSessions(pool, params) {
|
|
|
148
755
|
where += ' AND COALESCE(end_time, start_time) <= ?';
|
|
149
756
|
values.push(params.timeTo);
|
|
150
757
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
where += " AND LOWER(COALESCE(session_id,'')) NOT IN ('unknown','unkown','-')";
|
|
154
|
-
const [countRows] = await pool.query('SELECT COUNT(*) as cnt FROM audit_sessions WHERE ' + where, values);
|
|
155
|
-
const [rows] = await pool.query(`SELECT * FROM audit_sessions WHERE ${where}
|
|
758
|
+
const [countRows] = await pool.query('SELECT COUNT(*) as cnt FROM observation_sessions WHERE ' + where, values);
|
|
759
|
+
const [rows] = await pool.query(`SELECT * FROM observation_sessions WHERE ${where}
|
|
156
760
|
ORDER BY
|
|
157
761
|
CASE WHEN LOWER(COALESCE(user_id,'')) = 'system' THEN 1 ELSE 0 END ASC,
|
|
158
762
|
COALESCE(end_time, start_time) DESC,
|
|
159
763
|
start_time DESC
|
|
160
764
|
LIMIT ? OFFSET ?`, [...values, limit, offset]);
|
|
765
|
+
const sessions = rows;
|
|
766
|
+
const sessionIds = sessions.map((s) => String(s.session_id || '')).filter(Boolean);
|
|
767
|
+
const subagentBySession = new Set();
|
|
768
|
+
if (sessionIds.length > 0) {
|
|
769
|
+
const placeholders = sessionIds.map(() => '?').join(',');
|
|
770
|
+
const [tagRows] = await pool.query(`SELECT DISTINCT session_id
|
|
771
|
+
FROM observation_actions
|
|
772
|
+
WHERE scope_id = ?
|
|
773
|
+
AND session_id IN (${placeholders})
|
|
774
|
+
AND action_type IN ('prompt_build', 'message')
|
|
775
|
+
AND (
|
|
776
|
+
input_params LIKE '%[Subagent Context]%'
|
|
777
|
+
OR input_params LIKE '%"source":"subagent"%'
|
|
778
|
+
)`, [scopeId, ...sessionIds]);
|
|
779
|
+
tagRows.forEach((r) => {
|
|
780
|
+
if (r && r.session_id)
|
|
781
|
+
subagentBySession.add(String(r.session_id));
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
const normalizedSessions = sessions.map((s) => {
|
|
785
|
+
const parent = String(s.parent_session_id || '').trim();
|
|
786
|
+
const sid = String(s.session_id || '');
|
|
787
|
+
const inferred = parent.length > 0 || subagentBySession.has(sid);
|
|
788
|
+
return {
|
|
789
|
+
...s,
|
|
790
|
+
is_subagent: inferred ? 1 : 0,
|
|
791
|
+
};
|
|
792
|
+
});
|
|
161
793
|
return {
|
|
162
|
-
sessions:
|
|
794
|
+
sessions: normalizedSessions,
|
|
163
795
|
total: Number(countRows[0]?.cnt ?? 0),
|
|
164
796
|
};
|
|
165
797
|
}
|
|
166
798
|
/** Get actions for a session (supports field selection and limit) */
|
|
167
799
|
async function getSessionActions(pool, sessionId, options) {
|
|
800
|
+
const scopeId = normalizeScopeId(options?.scopeId);
|
|
168
801
|
// If fields=action_type, return only minimal fields (for mini trace)
|
|
169
802
|
const selectCols = options?.fields === 'action_type'
|
|
170
803
|
? 'action_type, created_at'
|
|
@@ -173,8 +806,12 @@ async function getSessionActions(pool, sessionId, options) {
|
|
|
173
806
|
? Math.min(normalizePositiveInt(options.limit, 1000), 1000)
|
|
174
807
|
: undefined;
|
|
175
808
|
const limitClause = limitedRows ? ` LIMIT ${limitedRows}` : '';
|
|
176
|
-
let where = 'session_id = ?';
|
|
177
|
-
const values = [sessionId];
|
|
809
|
+
let where = 'scope_id = ? AND session_id = ?';
|
|
810
|
+
const values = [scopeId, sessionId];
|
|
811
|
+
if (!String(sessionId || '').startsWith(REPLAY_SESSION_PREFIX)) {
|
|
812
|
+
where += ' AND session_id NOT LIKE ?';
|
|
813
|
+
values.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
814
|
+
}
|
|
178
815
|
if (options?.timeFrom) {
|
|
179
816
|
where += ' AND created_at >= ?';
|
|
180
817
|
values.push(options.timeFrom);
|
|
@@ -183,16 +820,19 @@ async function getSessionActions(pool, sessionId, options) {
|
|
|
183
820
|
where += ' AND created_at <= ?';
|
|
184
821
|
values.push(options.timeTo);
|
|
185
822
|
}
|
|
186
|
-
const [rows] = await pool.query(`SELECT ${selectCols} FROM
|
|
823
|
+
const [rows] = await pool.query(`SELECT ${selectCols} FROM observation_actions WHERE ${where} ORDER BY created_at ASC, id ASC${limitClause}`, values);
|
|
187
824
|
return rows;
|
|
188
825
|
}
|
|
189
826
|
/** Get action list */
|
|
190
827
|
async function getActions(pool, params) {
|
|
828
|
+
const scopeId = normalizeScopeId(params.scopeId);
|
|
191
829
|
const page = normalizePositiveInt(params.page, 1);
|
|
192
830
|
const limit = Math.min(normalizePositiveInt(params.limit, 50), 200);
|
|
193
831
|
const offset = (page - 1) * limit;
|
|
194
|
-
let where = '
|
|
195
|
-
const values = [];
|
|
832
|
+
let where = 'scope_id = ?';
|
|
833
|
+
const values = [scopeId];
|
|
834
|
+
where += ' AND session_id NOT LIKE ?';
|
|
835
|
+
values.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
196
836
|
if (params.actionType) {
|
|
197
837
|
where += ' AND action_type = ?';
|
|
198
838
|
values.push(params.actionType);
|
|
@@ -209,8 +849,8 @@ async function getActions(pool, params) {
|
|
|
209
849
|
where += ' AND created_at <= ?';
|
|
210
850
|
values.push(params.timeTo);
|
|
211
851
|
}
|
|
212
|
-
const [countRows] = await pool.query('SELECT COUNT(*) as cnt FROM
|
|
213
|
-
const [rows] = await pool.query('SELECT * FROM
|
|
852
|
+
const [countRows] = await pool.query('SELECT COUNT(*) as cnt FROM observation_actions WHERE ' + where, values);
|
|
853
|
+
const [rows] = await pool.query('SELECT * FROM observation_actions WHERE ' + where + ' ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?', [...values, limit, offset]);
|
|
214
854
|
return {
|
|
215
855
|
actions: rows,
|
|
216
856
|
total: Number(countRows[0]?.cnt ?? 0),
|
|
@@ -221,11 +861,14 @@ function normalizeAlertStatus(status) {
|
|
|
221
861
|
}
|
|
222
862
|
/** Get security alert list */
|
|
223
863
|
async function getAlerts(pool, params) {
|
|
864
|
+
const scopeId = normalizeScopeId(params.scopeId);
|
|
224
865
|
const page = normalizePositiveInt(params.page, 1);
|
|
225
866
|
const limit = Math.min(normalizePositiveInt(params.limit, 20), 100);
|
|
226
867
|
const offset = (page - 1) * limit;
|
|
227
|
-
let where = '
|
|
228
|
-
const values = [];
|
|
868
|
+
let where = 'scope_id = ?';
|
|
869
|
+
const values = [scopeId];
|
|
870
|
+
where += ' AND session_id NOT LIKE ?';
|
|
871
|
+
values.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
229
872
|
if (params.severity) {
|
|
230
873
|
where += ' AND severity = ?';
|
|
231
874
|
values.push(params.severity);
|
|
@@ -269,8 +912,8 @@ async function getAlerts(pool, params) {
|
|
|
269
912
|
where += ' AND created_at <= ?';
|
|
270
913
|
values.push(params.timeTo);
|
|
271
914
|
}
|
|
272
|
-
const [countRows] = await pool.query('SELECT COUNT(*) as cnt FROM
|
|
273
|
-
const [rows] = await pool.query(`SELECT * FROM
|
|
915
|
+
const [countRows] = await pool.query('SELECT COUNT(*) as cnt FROM observation_alerts WHERE ' + where, values);
|
|
916
|
+
const [rows] = await pool.query(`SELECT * FROM observation_alerts WHERE ${where}
|
|
274
917
|
ORDER BY
|
|
275
918
|
CASE severity
|
|
276
919
|
WHEN 'critical' THEN 0
|
|
@@ -291,8 +934,11 @@ async function getAlerts(pool, params) {
|
|
|
291
934
|
}
|
|
292
935
|
/** Get security alert statistics */
|
|
293
936
|
async function getAlertStats(pool, params) {
|
|
294
|
-
|
|
295
|
-
|
|
937
|
+
const scopeId = normalizeScopeId(params?.scopeId);
|
|
938
|
+
let where = 'scope_id = ?';
|
|
939
|
+
const values = [scopeId];
|
|
940
|
+
where += ' AND session_id NOT LIKE ?';
|
|
941
|
+
values.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
296
942
|
if (params?.timeFrom) {
|
|
297
943
|
where += ' AND created_at >= ?';
|
|
298
944
|
values.push(params.timeFrom);
|
|
@@ -304,20 +950,20 @@ async function getAlertStats(pool, params) {
|
|
|
304
950
|
const [summaryRows] = await pool.query(`SELECT
|
|
305
951
|
COUNT(*) as total_cnt,
|
|
306
952
|
COALESCE(SUM(CASE WHEN created_at >= (NOW() - INTERVAL 24 HOUR) THEN 1 ELSE 0 END), 0) as recent_24h
|
|
307
|
-
FROM
|
|
953
|
+
FROM observation_alerts
|
|
308
954
|
WHERE ${where}`, values);
|
|
309
955
|
const [groupRows] = await pool.query(`SELECT 'severity' as grp, severity as k, COUNT(*) as cnt
|
|
310
|
-
FROM
|
|
956
|
+
FROM observation_alerts
|
|
311
957
|
WHERE ${where}
|
|
312
958
|
GROUP BY severity
|
|
313
959
|
UNION ALL
|
|
314
960
|
SELECT 'category' as grp, category as k, COUNT(*) as cnt
|
|
315
|
-
FROM
|
|
961
|
+
FROM observation_alerts
|
|
316
962
|
WHERE ${where}
|
|
317
963
|
GROUP BY category
|
|
318
964
|
UNION ALL
|
|
319
965
|
SELECT 'status' as grp, status as k, COUNT(*) as cnt
|
|
320
|
-
FROM
|
|
966
|
+
FROM observation_alerts
|
|
321
967
|
WHERE ${where}
|
|
322
968
|
GROUP BY status`, [...values, ...values, ...values]);
|
|
323
969
|
const bySeverity = {};
|
|
@@ -347,22 +993,28 @@ async function getAlertStats(pool, params) {
|
|
|
347
993
|
};
|
|
348
994
|
}
|
|
349
995
|
/** Update alert status */
|
|
350
|
-
async function updateAlertStatus(pool, alertId, status, resolvedBy) {
|
|
996
|
+
async function updateAlertStatus(pool, alertId, status, resolvedBy, scopeId) {
|
|
997
|
+
const scopedScopeId = normalizeScopeId(scopeId);
|
|
351
998
|
const validStatuses = ['open', 'acknowledged', 'resolved'];
|
|
352
999
|
if (!validStatuses.includes(status))
|
|
353
1000
|
return false;
|
|
354
1001
|
const resolvedAt = status === 'resolved'
|
|
355
1002
|
? new Date().toISOString().replace('T', ' ').slice(0, 19) // compatible with MySQL & DuckDB
|
|
356
1003
|
: null;
|
|
357
|
-
await pool.query(`UPDATE
|
|
1004
|
+
await pool.query(`UPDATE observation_alerts SET status = ?, resolved_by = ?, resolved_at = ? WHERE scope_id = ? AND alert_id = ?`, [status, resolvedBy || null, resolvedAt, scopedScopeId, alertId]);
|
|
358
1005
|
// Verify update result (compatible with MySQL and DuckDB, not relying on affectedRows)
|
|
359
|
-
const [rows] = await pool.query('SELECT 1 FROM
|
|
1006
|
+
const [rows] = await pool.query('SELECT 1 FROM observation_alerts WHERE scope_id = ? AND alert_id = ? AND status = ?', [scopedScopeId, alertId, status]);
|
|
360
1007
|
return rows.length > 0;
|
|
361
1008
|
}
|
|
362
1009
|
/** Get alerts for a session */
|
|
363
1010
|
async function getSessionAlerts(pool, sessionId, params) {
|
|
364
|
-
|
|
365
|
-
|
|
1011
|
+
const scopeId = normalizeScopeId(params?.scopeId);
|
|
1012
|
+
let where = 'scope_id = ? AND session_id = ?';
|
|
1013
|
+
const values = [scopeId, sessionId];
|
|
1014
|
+
if (!String(sessionId || '').startsWith(REPLAY_SESSION_PREFIX)) {
|
|
1015
|
+
where += ' AND session_id NOT LIKE ?';
|
|
1016
|
+
values.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
1017
|
+
}
|
|
366
1018
|
if (params?.timeFrom) {
|
|
367
1019
|
where += ' AND created_at >= ?';
|
|
368
1020
|
values.push(params.timeFrom);
|
|
@@ -371,7 +1023,7 @@ async function getSessionAlerts(pool, sessionId, params) {
|
|
|
371
1023
|
where += ' AND created_at <= ?';
|
|
372
1024
|
values.push(params.timeTo);
|
|
373
1025
|
}
|
|
374
|
-
const [rows] = await pool.query(`SELECT * FROM
|
|
1026
|
+
const [rows] = await pool.query(`SELECT * FROM observation_alerts WHERE ${where} ORDER BY created_at ASC`, values);
|
|
375
1027
|
return rows.map((r) => ({
|
|
376
1028
|
...r,
|
|
377
1029
|
status: normalizeAlertStatus(String(r.status || '')),
|
|
@@ -379,10 +1031,15 @@ async function getSessionAlerts(pool, sessionId, params) {
|
|
|
379
1031
|
}
|
|
380
1032
|
/** Get comprehensive analytics data */
|
|
381
1033
|
async function getAnalytics(pool, params) {
|
|
382
|
-
|
|
383
|
-
let
|
|
384
|
-
|
|
385
|
-
const
|
|
1034
|
+
const scopeId = normalizeScopeId(params.scopeId);
|
|
1035
|
+
let timeWhere = 'scope_id = ?';
|
|
1036
|
+
let sessTimeWhere = 'scope_id = ?';
|
|
1037
|
+
const timeVals = [scopeId];
|
|
1038
|
+
const sessTimeVals = [scopeId];
|
|
1039
|
+
timeWhere += ' AND session_id NOT LIKE ?';
|
|
1040
|
+
sessTimeWhere += ' AND session_id NOT LIKE ?';
|
|
1041
|
+
timeVals.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
1042
|
+
sessTimeVals.push(`${REPLAY_SESSION_PREFIX}%`);
|
|
386
1043
|
if (params.timeFrom) {
|
|
387
1044
|
timeWhere += ' AND created_at >= ?';
|
|
388
1045
|
sessTimeWhere += ' AND start_time >= ?';
|
|
@@ -396,17 +1053,17 @@ async function getAnalytics(pool, params) {
|
|
|
396
1053
|
sessTimeVals.push(params.timeTo);
|
|
397
1054
|
}
|
|
398
1055
|
// 1. Overview KPIs
|
|
399
|
-
const [sessCount] = await pool.query('SELECT COUNT(*) as cnt FROM
|
|
1056
|
+
const [sessCount] = await pool.query('SELECT COUNT(*) as cnt FROM observation_sessions WHERE ' + sessTimeWhere, sessTimeVals);
|
|
400
1057
|
const [actAgg] = await pool.query(`SELECT COUNT(*) as cnt,
|
|
401
1058
|
COALESCE(SUM(COALESCE(prompt_tokens,0)), 0) as inp,
|
|
402
1059
|
COALESCE(SUM(COALESCE(completion_tokens,0)), 0) as outp,
|
|
403
1060
|
COALESCE(AVG(NULLIF(duration_ms, 0)), 0) as avg_lat
|
|
404
|
-
FROM
|
|
405
|
-
const [modelCount] = await pool.query(`SELECT COUNT(DISTINCT model_name) as cnt FROM
|
|
1061
|
+
FROM observation_actions WHERE ` + timeWhere, timeVals);
|
|
1062
|
+
const [modelCount] = await pool.query(`SELECT COUNT(DISTINCT model_name) as cnt FROM observation_actions
|
|
406
1063
|
WHERE model_name != '' AND ` + timeWhere, timeVals);
|
|
407
1064
|
let secAlerts = 0;
|
|
408
1065
|
try {
|
|
409
|
-
const [alertCount] = await pool.query('SELECT COUNT(*) as cnt FROM
|
|
1066
|
+
const [alertCount] = await pool.query('SELECT COUNT(*) as cnt FROM observation_alerts WHERE ' + timeWhere, timeVals);
|
|
410
1067
|
secAlerts = Number(alertCount[0]?.cnt ?? 0);
|
|
411
1068
|
}
|
|
412
1069
|
catch { /* alerts table may not exist */ }
|
|
@@ -430,16 +1087,17 @@ async function getAnalytics(pool, params) {
|
|
|
430
1087
|
if (spanMs <= 48 * 3600 * 1000)
|
|
431
1088
|
granularity = 'hour';
|
|
432
1089
|
}
|
|
433
|
-
// Build GROUP BY expressions compatible with both MySQL and DuckDB
|
|
1090
|
+
// Build GROUP BY expressions compatible with both MySQL and DuckDB-like backends.
|
|
1091
|
+
// Avoid CAST(... AS VARCHAR) because some MySQL-compatible engines reject it.
|
|
434
1092
|
const sessGroupExpr = granularity === 'hour'
|
|
435
|
-
? `
|
|
436
|
-
: `
|
|
1093
|
+
? `start_time` // full timestamp, truncated in JS
|
|
1094
|
+
: `DATE(start_time)`;
|
|
437
1095
|
const actGroupExpr = granularity === 'hour'
|
|
438
|
-
? `
|
|
439
|
-
: `
|
|
1096
|
+
? `created_at`
|
|
1097
|
+
: `DATE(created_at)`;
|
|
440
1098
|
const [tsRows] = await pool.query(`SELECT ${sessGroupExpr} as bucket,
|
|
441
1099
|
COUNT(*) as sessions
|
|
442
|
-
FROM
|
|
1100
|
+
FROM observation_sessions
|
|
443
1101
|
WHERE ` + sessTimeWhere + `
|
|
444
1102
|
GROUP BY ${sessGroupExpr}
|
|
445
1103
|
ORDER BY bucket ASC`, sessTimeVals);
|
|
@@ -447,7 +1105,7 @@ async function getAnalytics(pool, params) {
|
|
|
447
1105
|
COUNT(*) as actions,
|
|
448
1106
|
COALESCE(SUM(COALESCE(prompt_tokens,0)),0) as inp,
|
|
449
1107
|
COALESCE(SUM(COALESCE(completion_tokens,0)),0) as outp
|
|
450
|
-
FROM
|
|
1108
|
+
FROM observation_actions
|
|
451
1109
|
WHERE ` + timeWhere + `
|
|
452
1110
|
GROUP BY ${actGroupExpr}
|
|
453
1111
|
ORDER BY bucket ASC`, timeVals);
|
|
@@ -519,7 +1177,7 @@ async function getAnalytics(pool, params) {
|
|
|
519
1177
|
COALESCE(SUM(COALESCE(prompt_tokens,0)),0) as inp,
|
|
520
1178
|
COALESCE(SUM(COALESCE(completion_tokens,0)),0) as outp,
|
|
521
1179
|
COALESCE(AVG(NULLIF(duration_ms,0)),0) as avg_lat
|
|
522
|
-
FROM
|
|
1180
|
+
FROM observation_actions
|
|
523
1181
|
WHERE action_type = 'message' AND model_name != '' AND ` + timeWhere + `
|
|
524
1182
|
GROUP BY model_name
|
|
525
1183
|
ORDER BY inp + outp DESC
|
|
@@ -537,7 +1195,7 @@ async function getAnalytics(pool, params) {
|
|
|
537
1195
|
output: Number(r.outp),
|
|
538
1196
|
}));
|
|
539
1197
|
// 4. Action distribution
|
|
540
|
-
const [actTypeRows] = await pool.query(`SELECT action_type, COUNT(*) as cnt FROM
|
|
1198
|
+
const [actTypeRows] = await pool.query(`SELECT action_type, COUNT(*) as cnt FROM observation_actions
|
|
541
1199
|
WHERE ` + timeWhere + `
|
|
542
1200
|
GROUP BY action_type ORDER BY cnt DESC`, timeVals);
|
|
543
1201
|
const actionDistribution = {};
|
|
@@ -548,7 +1206,7 @@ async function getAnalytics(pool, params) {
|
|
|
548
1206
|
const [topAgentRows] = await pool.query(`SELECT b.user_id, b.sessions, b.tokens, COALESCE(COUNT(a.id), 0) as actions
|
|
549
1207
|
FROM (
|
|
550
1208
|
SELECT user_id, COUNT(DISTINCT session_id) as sessions, COALESCE(SUM(total_tokens), 0) as tokens
|
|
551
|
-
FROM
|
|
1209
|
+
FROM observation_sessions
|
|
552
1210
|
WHERE user_id != ''
|
|
553
1211
|
AND LOWER(user_id) NOT IN ('unknown','unkown')
|
|
554
1212
|
AND ` + sessTimeWhere + `
|
|
@@ -558,7 +1216,7 @@ async function getAnalytics(pool, params) {
|
|
|
558
1216
|
sessions DESC
|
|
559
1217
|
LIMIT 10
|
|
560
1218
|
) b
|
|
561
|
-
LEFT JOIN
|
|
1219
|
+
LEFT JOIN observation_actions a
|
|
562
1220
|
ON a.user_id = b.user_id
|
|
563
1221
|
AND ` + timeWhere + `
|
|
564
1222
|
GROUP BY b.user_id, b.sessions, b.tokens
|
|
@@ -573,7 +1231,134 @@ async function getAnalytics(pool, params) {
|
|
|
573
1231
|
}));
|
|
574
1232
|
return { overview, timeSeries, granularity, modelUsage, actionDistribution, topAgents, tokensByModel };
|
|
575
1233
|
}
|
|
1234
|
+
function buildBucketTimeline(fromMs, toMs, stepMs) {
|
|
1235
|
+
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || !Number.isFinite(stepMs) || stepMs <= 0)
|
|
1236
|
+
return [];
|
|
1237
|
+
const start = Math.floor(fromMs / stepMs) * stepMs;
|
|
1238
|
+
const end = Math.floor(toMs / stepMs) * stepMs;
|
|
1239
|
+
const out = [];
|
|
1240
|
+
for (let ts = start; ts <= end; ts += stepMs)
|
|
1241
|
+
out.push(ts);
|
|
1242
|
+
return out;
|
|
1243
|
+
}
|
|
1244
|
+
function inferFillMode(temporality) {
|
|
1245
|
+
const t = String(temporality || '').trim().toLowerCase();
|
|
1246
|
+
return t === 'delta' ? 'zero' : 'carry';
|
|
1247
|
+
}
|
|
1248
|
+
function densifySeriesPoints(points, timeline, fillMode, seedValue) {
|
|
1249
|
+
if (!Array.isArray(points) || !points.length || !Array.isArray(timeline) || !timeline.length)
|
|
1250
|
+
return [];
|
|
1251
|
+
const byTs = new Map();
|
|
1252
|
+
for (const p of points) {
|
|
1253
|
+
const ts = Number(p.timestampMs || 0);
|
|
1254
|
+
const v = Number(p.value || 0);
|
|
1255
|
+
if (!Number.isFinite(ts) || !Number.isFinite(v))
|
|
1256
|
+
continue;
|
|
1257
|
+
byTs.set(ts, v);
|
|
1258
|
+
}
|
|
1259
|
+
let lastKnown = Number.isFinite(seedValue) ? Number(seedValue) : null;
|
|
1260
|
+
if (fillMode === 'carry' && lastKnown == null) {
|
|
1261
|
+
let firstTs = Infinity;
|
|
1262
|
+
let firstVal = null;
|
|
1263
|
+
for (const [ts, v] of byTs.entries()) {
|
|
1264
|
+
if (ts < firstTs) {
|
|
1265
|
+
firstTs = ts;
|
|
1266
|
+
firstVal = v;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (Number.isFinite(firstTs) && firstVal != null && Number.isFinite(firstVal)) {
|
|
1270
|
+
lastKnown = Number(firstVal);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const out = [];
|
|
1274
|
+
for (const ts of timeline) {
|
|
1275
|
+
if (byTs.has(ts)) {
|
|
1276
|
+
const v = Number(byTs.get(ts) || 0);
|
|
1277
|
+
lastKnown = v;
|
|
1278
|
+
out.push({ timestampMs: ts, value: v });
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (fillMode === 'zero') {
|
|
1282
|
+
out.push({ timestampMs: ts, value: 0 });
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (lastKnown != null) {
|
|
1286
|
+
out.push({ timestampMs: ts, value: lastKnown });
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return out;
|
|
1290
|
+
}
|
|
1291
|
+
function inPlaceholders(n) {
|
|
1292
|
+
return new Array(Math.max(0, n)).fill('?').join(', ');
|
|
1293
|
+
}
|
|
1294
|
+
async function fetchCarrySeedValuesV2(pool, params) {
|
|
1295
|
+
const ids = (params.seriesIds || []).filter(Boolean);
|
|
1296
|
+
if (!ids.length)
|
|
1297
|
+
return new Map();
|
|
1298
|
+
const placeholders = inPlaceholders(ids.length);
|
|
1299
|
+
const sql = `
|
|
1300
|
+
SELECT t.series_id AS series_id, t.v AS v
|
|
1301
|
+
FROM (
|
|
1302
|
+
SELECT
|
|
1303
|
+
q.series_id AS series_id,
|
|
1304
|
+
q.v AS v,
|
|
1305
|
+
q.ts_ms AS ts_ms,
|
|
1306
|
+
ROW_NUMBER() OVER (PARTITION BY q.series_id ORDER BY q.ts_ms DESC) AS rn
|
|
1307
|
+
FROM (
|
|
1308
|
+
SELECT s.series_id AS series_id, n.ts_ms AS ts_ms, n.value AS v
|
|
1309
|
+
FROM oc_metric_points_number n
|
|
1310
|
+
INNER JOIN oc_metric_series s ON s.series_id = n.series_id
|
|
1311
|
+
WHERE s.scope_id = ? AND n.scope_id = ? AND s.metric_name = ? AND n.ts_ms < ? AND s.series_id IN (${placeholders})
|
|
1312
|
+
UNION ALL
|
|
1313
|
+
SELECT s2.series_id AS series_id, h.ts_ms AS ts_ms, CAST(h.count AS DOUBLE) AS v
|
|
1314
|
+
FROM oc_metric_points_histogram h
|
|
1315
|
+
INNER JOIN oc_metric_series s2 ON s2.series_id = h.series_id
|
|
1316
|
+
WHERE s2.scope_id = ? AND h.scope_id = ? AND s2.metric_name = ? AND h.ts_ms < ? AND s2.series_id IN (${placeholders})
|
|
1317
|
+
) q
|
|
1318
|
+
) t
|
|
1319
|
+
WHERE t.rn = 1
|
|
1320
|
+
`;
|
|
1321
|
+
const bind = [
|
|
1322
|
+
params.scopeId, params.scopeId, params.metricName, params.beforeMs, ...ids,
|
|
1323
|
+
params.scopeId, params.scopeId, params.metricName, params.beforeMs, ...ids,
|
|
1324
|
+
];
|
|
1325
|
+
const [rows] = await pool.query(sql, bind);
|
|
1326
|
+
const out = new Map();
|
|
1327
|
+
for (const r of rows) {
|
|
1328
|
+
const sid = String(r.series_id || '');
|
|
1329
|
+
const v = Number(r.v || 0);
|
|
1330
|
+
if (!sid || !Number.isFinite(v))
|
|
1331
|
+
continue;
|
|
1332
|
+
out.set(sid, v);
|
|
1333
|
+
}
|
|
1334
|
+
return out;
|
|
1335
|
+
}
|
|
1336
|
+
function aggregateSeriesPoints(series, timeline) {
|
|
1337
|
+
if (!Array.isArray(series) || !series.length || !Array.isArray(timeline) || !timeline.length)
|
|
1338
|
+
return [];
|
|
1339
|
+
const seriesMaps = series.map((s) => {
|
|
1340
|
+
const m = new Map();
|
|
1341
|
+
for (const p of s.points || [])
|
|
1342
|
+
m.set(Number(p.timestampMs || 0), Number(p.value || 0));
|
|
1343
|
+
return m;
|
|
1344
|
+
});
|
|
1345
|
+
const out = [];
|
|
1346
|
+
for (const ts of timeline) {
|
|
1347
|
+
let sum = 0;
|
|
1348
|
+
let hasAny = false;
|
|
1349
|
+
for (const m of seriesMaps) {
|
|
1350
|
+
if (!m.has(ts))
|
|
1351
|
+
continue;
|
|
1352
|
+
sum += Number(m.get(ts) || 0);
|
|
1353
|
+
hasAny = true;
|
|
1354
|
+
}
|
|
1355
|
+
if (hasAny)
|
|
1356
|
+
out.push({ timestampMs: ts, value: sum });
|
|
1357
|
+
}
|
|
1358
|
+
return out;
|
|
1359
|
+
}
|
|
576
1360
|
async function getMetricsOverview(pool, params) {
|
|
1361
|
+
const scopeId = normalizeScopeId(params?.scopeId);
|
|
577
1362
|
const minutes = Math.min(Math.max(normalizePositiveInt(params?.minutes, 60), 1), 24 * 60);
|
|
578
1363
|
const limit = Math.min(Math.max(normalizePositiveInt(params?.limit, 100), 1), 500);
|
|
579
1364
|
const window = resolveMetricsWindow({
|
|
@@ -587,11 +1372,11 @@ async function getMetricsOverview(pool, params) {
|
|
|
587
1372
|
const [summaryRows] = await pool.query(`SELECT
|
|
588
1373
|
COUNT(DISTINCT s.metric_name) AS metric_cnt,
|
|
589
1374
|
(
|
|
590
|
-
COALESCE((SELECT COUNT(*) FROM oc_metric_points_number pn WHERE pn.ts_ms >= ? AND pn.ts_ms <= ?), 0) +
|
|
591
|
-
COALESCE((SELECT COUNT(*) FROM oc_metric_points_histogram ph WHERE ph.ts_ms >= ? AND ph.ts_ms <= ?), 0)
|
|
1375
|
+
COALESCE((SELECT COUNT(*) FROM oc_metric_points_number pn WHERE pn.scope_id = ? AND pn.ts_ms >= ? AND pn.ts_ms <= ?), 0) +
|
|
1376
|
+
COALESCE((SELECT COUNT(*) FROM oc_metric_points_histogram ph WHERE ph.scope_id = ? AND ph.ts_ms >= ? AND ph.ts_ms <= ?), 0)
|
|
592
1377
|
) AS cnt
|
|
593
1378
|
FROM oc_metric_series s
|
|
594
|
-
WHERE s.last_seen_ms >= ? AND s.last_seen_ms <= ?`, [fromMs, toMs, fromMs, toMs, fromMs, toMs]);
|
|
1379
|
+
WHERE s.scope_id = ? AND s.last_seen_ms >= ? AND s.last_seen_ms <= ?`, [scopeId, fromMs, toMs, scopeId, fromMs, toMs, scopeId, fromMs, toMs]);
|
|
595
1380
|
const [catalogRows] = await pool.query(`SELECT
|
|
596
1381
|
s.metric_name AS metric_name,
|
|
597
1382
|
MAX(s.metric_type) AS metric_type,
|
|
@@ -602,9 +1387,9 @@ async function getMetricsOverview(pool, params) {
|
|
|
602
1387
|
LEFT JOIN (
|
|
603
1388
|
SELECT series_id, COUNT(*) AS samples, MAX(ts_ms) AS latest_ts
|
|
604
1389
|
FROM (
|
|
605
|
-
SELECT series_id, ts_ms FROM oc_metric_points_number WHERE ts_ms >= ? AND ts_ms <= ?
|
|
1390
|
+
SELECT series_id, ts_ms FROM oc_metric_points_number WHERE scope_id = ? AND ts_ms >= ? AND ts_ms <= ?
|
|
606
1391
|
UNION ALL
|
|
607
|
-
SELECT series_id, ts_ms FROM oc_metric_points_histogram WHERE ts_ms >= ? AND ts_ms <= ?
|
|
1392
|
+
SELECT series_id, ts_ms FROM oc_metric_points_histogram WHERE scope_id = ? AND ts_ms >= ? AND ts_ms <= ?
|
|
608
1393
|
) t
|
|
609
1394
|
GROUP BY series_id
|
|
610
1395
|
) a ON a.series_id = s.series_id
|
|
@@ -612,10 +1397,10 @@ async function getMetricsOverview(pool, params) {
|
|
|
612
1397
|
ON n.series_id = s.series_id AND n.ts_ms = a.latest_ts
|
|
613
1398
|
LEFT JOIN oc_metric_points_histogram h
|
|
614
1399
|
ON h.series_id = s.series_id AND h.ts_ms = a.latest_ts
|
|
615
|
-
WHERE s.last_seen_ms >= ? AND s.last_seen_ms <= ?
|
|
1400
|
+
WHERE s.scope_id = ? AND s.last_seen_ms >= ? AND s.last_seen_ms <= ?
|
|
616
1401
|
GROUP BY s.metric_name, a.samples, a.latest_ts
|
|
617
1402
|
ORDER BY s.metric_name ASC
|
|
618
|
-
LIMIT ?`, [fromMs, toMs, fromMs, toMs, fromMs, toMs, limit]);
|
|
1403
|
+
LIMIT ?`, [scopeId, fromMs, toMs, scopeId, fromMs, toMs, scopeId, fromMs, toMs, limit]);
|
|
619
1404
|
const items = catalogRows.map((r) => ({
|
|
620
1405
|
metricName: String(r.metric_name || ''),
|
|
621
1406
|
metricType: String(r.metric_type || 'untyped'),
|
|
@@ -623,18 +1408,25 @@ async function getMetricsOverview(pool, params) {
|
|
|
623
1408
|
latestTimestampMs: Number(r.latest_ts || 0),
|
|
624
1409
|
latestValue: Number(r.latest_value || 0),
|
|
625
1410
|
}));
|
|
626
|
-
|
|
1411
|
+
const result = {
|
|
627
1412
|
totalMetrics: Number(summaryRows[0]?.metric_cnt ?? 0),
|
|
628
1413
|
totalSamples: Number(summaryRows[0]?.cnt ?? 0),
|
|
629
1414
|
rangeMinutes: window.rangeMinutes,
|
|
630
1415
|
items,
|
|
631
1416
|
};
|
|
1417
|
+
// Some deployments still persist only legacy snapshots.
|
|
1418
|
+
// If new-series tables return empty, fall back to legacy query path.
|
|
1419
|
+
if (result.totalMetrics <= 0 && result.totalSamples <= 0 && items.length === 0) {
|
|
1420
|
+
return getMetricsOverviewLegacy(pool, window.rangeMinutes, limit, fromMs, toMs);
|
|
1421
|
+
}
|
|
1422
|
+
return result;
|
|
632
1423
|
}
|
|
633
1424
|
catch {
|
|
634
1425
|
return getMetricsOverviewLegacy(pool, window.rangeMinutes, limit, fromMs, toMs);
|
|
635
1426
|
}
|
|
636
1427
|
}
|
|
637
1428
|
async function getMetricSeries(pool, params) {
|
|
1429
|
+
const scopeId = normalizeScopeId(params.scopeId);
|
|
638
1430
|
const metricName = params.metricName?.trim();
|
|
639
1431
|
if (!metricName) {
|
|
640
1432
|
return {
|
|
@@ -656,27 +1448,28 @@ async function getMetricSeries(pool, params) {
|
|
|
656
1448
|
try {
|
|
657
1449
|
const [typeRows] = await pool.query(`SELECT metric_type
|
|
658
1450
|
FROM oc_metric_series
|
|
659
|
-
WHERE metric_name = ?
|
|
1451
|
+
WHERE scope_id = ? AND metric_name = ?
|
|
660
1452
|
ORDER BY last_seen_ms DESC
|
|
661
|
-
LIMIT 1`, [metricName]);
|
|
1453
|
+
LIMIT 1`, [scopeId, metricName]);
|
|
662
1454
|
const [rows] = await pool.query(`SELECT
|
|
663
|
-
|
|
1455
|
+
FLOOR(q.ts_ms / ?) * ? AS bucket_ms,
|
|
664
1456
|
q.series_id AS series_id,
|
|
665
1457
|
q.labels_json AS labels_json,
|
|
1458
|
+
q.temporality AS temporality,
|
|
666
1459
|
MAX(q.v) AS series_bucket_max
|
|
667
1460
|
FROM (
|
|
668
|
-
SELECT n.ts_ms AS ts_ms, n.value AS v, s.series_id AS series_id, s.labels_json AS labels_json
|
|
1461
|
+
SELECT n.ts_ms AS ts_ms, n.value AS v, s.series_id AS series_id, s.labels_json AS labels_json, s.temporality AS temporality
|
|
669
1462
|
FROM oc_metric_points_number n
|
|
670
1463
|
INNER JOIN oc_metric_series s ON s.series_id = n.series_id
|
|
671
|
-
WHERE s.metric_name = ? AND n.ts_ms >= ? AND n.ts_ms <= ?
|
|
1464
|
+
WHERE s.scope_id = ? AND n.scope_id = ? AND s.metric_name = ? AND n.ts_ms >= ? AND n.ts_ms <= ?
|
|
672
1465
|
UNION ALL
|
|
673
|
-
SELECT h.ts_ms AS ts_ms, CAST(h.count AS DOUBLE) AS v, s2.series_id AS series_id, s2.labels_json AS labels_json
|
|
1466
|
+
SELECT h.ts_ms AS ts_ms, CAST(h.count AS DOUBLE) AS v, s2.series_id AS series_id, s2.labels_json AS labels_json, s2.temporality AS temporality
|
|
674
1467
|
FROM oc_metric_points_histogram h
|
|
675
1468
|
INNER JOIN oc_metric_series s2 ON s2.series_id = h.series_id
|
|
676
|
-
WHERE s2.metric_name = ? AND h.ts_ms >= ? AND h.ts_ms <= ?
|
|
1469
|
+
WHERE s2.scope_id = ? AND h.scope_id = ? AND s2.metric_name = ? AND h.ts_ms >= ? AND h.ts_ms <= ?
|
|
677
1470
|
) q
|
|
678
|
-
GROUP BY bucket_ms, q.series_id, q.labels_json
|
|
679
|
-
ORDER BY bucket_ms ASC, q.series_id ASC`, [stepMs, stepMs, metricName, fromMs, toMs, metricName, fromMs, toMs]);
|
|
1471
|
+
GROUP BY bucket_ms, q.series_id, q.labels_json, q.temporality
|
|
1472
|
+
ORDER BY bucket_ms ASC, q.series_id ASC`, [stepMs, stepMs, scopeId, scopeId, metricName, fromMs, toMs, scopeId, scopeId, metricName, fromMs, toMs]);
|
|
680
1473
|
const seriesMap = new Map();
|
|
681
1474
|
for (const r of rows) {
|
|
682
1475
|
const seriesId = String(r.series_id || '');
|
|
@@ -689,6 +1482,7 @@ async function getMetricSeries(pool, params) {
|
|
|
689
1482
|
entry = {
|
|
690
1483
|
seriesId,
|
|
691
1484
|
labels: parseMetricLabelsJson(r.labels_json),
|
|
1485
|
+
temporality: String(r.temporality || ''),
|
|
692
1486
|
points: [],
|
|
693
1487
|
latestTimestampMs: 0,
|
|
694
1488
|
samples: 0,
|
|
@@ -700,23 +1494,31 @@ async function getMetricSeries(pool, params) {
|
|
|
700
1494
|
if (timestampMs > entry.latestTimestampMs)
|
|
701
1495
|
entry.latestTimestampMs = timestampMs;
|
|
702
1496
|
}
|
|
703
|
-
const
|
|
1497
|
+
const timeline = buildBucketTimeline(fromMs, toMs, stepMs);
|
|
1498
|
+
const rawSeries = Array.from(seriesMap.values());
|
|
1499
|
+
const carrySeriesIds = rawSeries
|
|
1500
|
+
.filter((s) => inferFillMode(s.temporality) === 'carry')
|
|
1501
|
+
.map((s) => s.seriesId);
|
|
1502
|
+
const carrySeeds = carrySeriesIds.length
|
|
1503
|
+
? await fetchCarrySeedValuesV2(pool, { scopeId, metricName, beforeMs: fromMs, seriesIds: carrySeriesIds })
|
|
1504
|
+
: new Map();
|
|
1505
|
+
const series = rawSeries.map((s) => {
|
|
1506
|
+
const mode = inferFillMode(s.temporality);
|
|
1507
|
+
const seed = mode === 'carry' ? carrySeeds.get(s.seriesId) : undefined;
|
|
1508
|
+
const densified = densifySeriesPoints(s.points, timeline, mode, seed);
|
|
1509
|
+
return {
|
|
1510
|
+
...s,
|
|
1511
|
+
points: densified,
|
|
1512
|
+
};
|
|
1513
|
+
});
|
|
704
1514
|
let points = [];
|
|
705
1515
|
if (aggregate) {
|
|
706
|
-
|
|
707
|
-
for (const s of series) {
|
|
708
|
-
for (const p of s.points) {
|
|
709
|
-
bucketTotals.set(p.timestampMs, (bucketTotals.get(p.timestampMs) || 0) + p.value);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
points = Array.from(bucketTotals.entries())
|
|
713
|
-
.sort((a, b) => a[0] - b[0])
|
|
714
|
-
.map(([timestampMs, value]) => ({ timestampMs, value }));
|
|
1516
|
+
points = aggregateSeriesPoints(series, timeline);
|
|
715
1517
|
}
|
|
716
1518
|
else if (series.length === 1) {
|
|
717
1519
|
points = series[0].points.slice();
|
|
718
1520
|
}
|
|
719
|
-
|
|
1521
|
+
const result = {
|
|
720
1522
|
metricName,
|
|
721
1523
|
metricType: String(typeRows[0]?.metric_type || 'untyped'),
|
|
722
1524
|
rangeMinutes: window.rangeMinutes,
|
|
@@ -725,6 +1527,12 @@ async function getMetricSeries(pool, params) {
|
|
|
725
1527
|
series,
|
|
726
1528
|
aggregateApplied: aggregate,
|
|
727
1529
|
};
|
|
1530
|
+
// Backward compatibility: if v2 metrics tables are present but empty for this metric,
|
|
1531
|
+
// try legacy snapshot table so charts still render.
|
|
1532
|
+
if (result.series.length === 0 && result.points.length === 0) {
|
|
1533
|
+
return getMetricSeriesLegacy(pool, metricName, window.rangeMinutes, stepSec, stepMs, fromMs, toMs, aggregate);
|
|
1534
|
+
}
|
|
1535
|
+
return result;
|
|
728
1536
|
}
|
|
729
1537
|
catch {
|
|
730
1538
|
return getMetricSeriesLegacy(pool, metricName, window.rangeMinutes, stepSec, stepMs, fromMs, toMs, aggregate);
|
|
@@ -778,10 +1586,10 @@ async function getMetricSeriesLegacy(pool, metricName, minutes, stepSec, stepMs,
|
|
|
778
1586
|
agg.series_bucket_max AS series_bucket_max
|
|
779
1587
|
FROM (
|
|
780
1588
|
SELECT
|
|
781
|
-
|
|
1589
|
+
FLOOR(s.sample_timestamp_ms / ?) * ? AS bucket_ms,
|
|
782
1590
|
COALESCE(
|
|
783
1591
|
NULLIF(TRIM(COALESCE(s.label_fingerprint, '')), ''),
|
|
784
|
-
CONCAT('
|
|
1592
|
+
CONCAT('labels:', COALESCE(s.labels_json, '{}'))
|
|
785
1593
|
) AS series_key,
|
|
786
1594
|
COALESCE(s.labels_json, '{}') AS labels_json,
|
|
787
1595
|
MAX(s.value) AS series_bucket_max
|
|
@@ -789,15 +1597,9 @@ async function getMetricSeriesLegacy(pool, metricName, minutes, stepSec, stepMs,
|
|
|
789
1597
|
WHERE s.metric_name = ?
|
|
790
1598
|
AND s.sample_timestamp_ms >= ?
|
|
791
1599
|
AND s.sample_timestamp_ms <= ?
|
|
792
|
-
GROUP BY
|
|
793
|
-
CAST(FLOOR(s.sample_timestamp_ms / ?) * ? AS BIGINT),
|
|
794
|
-
COALESCE(
|
|
795
|
-
NULLIF(TRIM(COALESCE(s.label_fingerprint, '')), ''),
|
|
796
|
-
CONCAT('id:', CAST(s.id AS CHAR))
|
|
797
|
-
),
|
|
798
|
-
COALESCE(s.labels_json, '{}')
|
|
1600
|
+
GROUP BY 1, 2, 3
|
|
799
1601
|
) agg
|
|
800
|
-
ORDER BY agg.bucket_ms ASC, agg.series_key ASC`, [stepMs, stepMs, metricName, fromMs, toMs
|
|
1602
|
+
ORDER BY agg.bucket_ms ASC, agg.series_key ASC`, [stepMs, stepMs, metricName, fromMs, toMs]);
|
|
801
1603
|
const seriesMap = new Map();
|
|
802
1604
|
for (const r of rows) {
|
|
803
1605
|
const seriesId = String(r.series_id || '');
|
|
@@ -822,20 +1624,17 @@ async function getMetricSeriesLegacy(pool, metricName, minutes, stepSec, stepMs,
|
|
|
822
1624
|
entry.latestTimestampMs = timestampMs;
|
|
823
1625
|
}
|
|
824
1626
|
const series = Array.from(seriesMap.values());
|
|
1627
|
+
const timeline = buildBucketTimeline(fromMs, toMs, stepMs);
|
|
1628
|
+
const densifiedSeries = series.map((s) => ({
|
|
1629
|
+
...s,
|
|
1630
|
+
points: densifySeriesPoints(s.points, timeline, 'carry'),
|
|
1631
|
+
}));
|
|
825
1632
|
let points = [];
|
|
826
1633
|
if (aggregate) {
|
|
827
|
-
|
|
828
|
-
for (const s of series) {
|
|
829
|
-
for (const p of s.points) {
|
|
830
|
-
bucketTotals.set(p.timestampMs, (bucketTotals.get(p.timestampMs) || 0) + p.value);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
points = Array.from(bucketTotals.entries())
|
|
834
|
-
.sort((a, b) => a[0] - b[0])
|
|
835
|
-
.map(([timestampMs, value]) => ({ timestampMs, value }));
|
|
1634
|
+
points = aggregateSeriesPoints(densifiedSeries, timeline);
|
|
836
1635
|
}
|
|
837
|
-
else if (
|
|
838
|
-
points =
|
|
1636
|
+
else if (densifiedSeries.length === 1) {
|
|
1637
|
+
points = densifiedSeries[0].points.slice();
|
|
839
1638
|
}
|
|
840
1639
|
return {
|
|
841
1640
|
metricName,
|
|
@@ -843,7 +1642,7 @@ async function getMetricSeriesLegacy(pool, metricName, minutes, stepSec, stepMs,
|
|
|
843
1642
|
rangeMinutes: minutes,
|
|
844
1643
|
stepSec,
|
|
845
1644
|
points,
|
|
846
|
-
series,
|
|
1645
|
+
series: densifiedSeries,
|
|
847
1646
|
aggregateApplied: aggregate,
|
|
848
1647
|
};
|
|
849
1648
|
}
|
|
@@ -869,11 +1668,12 @@ function parseMetricLabelsJson(raw) {
|
|
|
869
1668
|
}
|
|
870
1669
|
}
|
|
871
1670
|
async function getTraceObservationTree(pool, traceId, params) {
|
|
1671
|
+
const scopeId = normalizeScopeId(params?.scopeId);
|
|
872
1672
|
const id = String(traceId || '').trim();
|
|
873
1673
|
if (!id)
|
|
874
1674
|
return { traceId: '', observations: [] };
|
|
875
|
-
let where = 'c.trace_id = ?';
|
|
876
|
-
const values = [id];
|
|
1675
|
+
let where = 'c.scope_id = ? AND c.trace_id = ?';
|
|
1676
|
+
const values = [scopeId, id];
|
|
877
1677
|
if (params?.timeFrom) {
|
|
878
1678
|
where += ' AND c.start_time >= ?';
|
|
879
1679
|
values.push(params.timeFrom);
|
|
@@ -886,6 +1686,7 @@ async function getTraceObservationTree(pool, traceId, params) {
|
|
|
886
1686
|
const [rows] = await pool.query(`SELECT
|
|
887
1687
|
c.observation_id AS observation_id,
|
|
888
1688
|
c.parent_observation_id AS parent_observation_id,
|
|
1689
|
+
pcore.run_id AS parent_run_id,
|
|
889
1690
|
c.root_observation_id AS root_observation_id,
|
|
890
1691
|
c.trace_id AS trace_id,
|
|
891
1692
|
c.observation_type AS observation_type,
|
|
@@ -912,6 +1713,8 @@ async function getTraceObservationTree(pool, traceId, params) {
|
|
|
912
1713
|
p.model_params_json AS model_params_json,
|
|
913
1714
|
p.error_json AS error_json
|
|
914
1715
|
FROM oc_observations_core c
|
|
1716
|
+
LEFT JOIN oc_observations_core pcore
|
|
1717
|
+
ON pcore.observation_id = c.parent_observation_id
|
|
915
1718
|
LEFT JOIN oc_observations_payload p
|
|
916
1719
|
ON p.observation_id = c.observation_id
|
|
917
1720
|
WHERE ${where}
|
|
@@ -921,6 +1724,7 @@ async function getTraceObservationTree(pool, traceId, params) {
|
|
|
921
1724
|
observations: rows.map((r) => ({
|
|
922
1725
|
observationId: String(r.observation_id || ''),
|
|
923
1726
|
parentObservationId: r.parent_observation_id ? String(r.parent_observation_id) : null,
|
|
1727
|
+
parentRunId: r.parent_run_id ? String(r.parent_run_id) : null,
|
|
924
1728
|
rootObservationId: String(r.root_observation_id || ''),
|
|
925
1729
|
traceId: String(r.trace_id || ''),
|
|
926
1730
|
type: String(r.observation_type || ''),
|