Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
173 lines
4.4 KiB
JavaScript
173 lines
4.4 KiB
JavaScript
/**
|
|
* monitoring/analytics.js
|
|
* Analytics and data management for MonitoringService
|
|
*/
|
|
|
|
import { dbGet, dbRun, dbAll } from '../../database/connection.js';
|
|
import { aiLogger } from '../../utils/logger.js';
|
|
|
|
/**
|
|
* Get analytics summary
|
|
*/
|
|
export async function getAnalyticsSummary(options = {}) {
|
|
const { startDate, endDate, userId } = options;
|
|
|
|
const conditions = [];
|
|
const values = [];
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
values.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
values.push(endDate);
|
|
}
|
|
|
|
if (userId) {
|
|
conditions.push('user_id = ?');
|
|
values.push(userId);
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
|
|
// Overall stats
|
|
const stats = await dbGet(`
|
|
SELECT
|
|
COUNT(*) as totalRuns,
|
|
COUNT(CASE WHEN status = 'success' THEN 1 END) as successfulRuns,
|
|
COUNT(CASE WHEN status = 'error' THEN 1 END) as failedRuns,
|
|
SUM(tokens_prompt) as totalPromptTokens,
|
|
SUM(tokens_completion) as totalCompletionTokens,
|
|
SUM(cost) as totalCost,
|
|
AVG(duration_ms) as avgDurationMs,
|
|
COUNT(DISTINCT user_id) as uniqueUsers
|
|
FROM monitoring_runs
|
|
${whereClause}
|
|
`, values);
|
|
|
|
// By type breakdown
|
|
const byType = await dbAll(`
|
|
SELECT
|
|
type,
|
|
COUNT(*) as count,
|
|
SUM(tokens_prompt + tokens_completion) as tokens,
|
|
SUM(cost) as cost,
|
|
AVG(duration_ms) as avgDuration
|
|
FROM monitoring_runs
|
|
${whereClause}
|
|
GROUP BY type
|
|
ORDER BY count DESC
|
|
`, values);
|
|
|
|
// By model breakdown
|
|
const byModel = await dbAll(`
|
|
SELECT
|
|
model,
|
|
COUNT(*) as count,
|
|
SUM(tokens_prompt + tokens_completion) as tokens,
|
|
SUM(cost) as cost
|
|
FROM monitoring_runs
|
|
${whereClause} ${whereClause ? 'AND' : 'WHERE'} model IS NOT NULL
|
|
GROUP BY model
|
|
ORDER BY count DESC
|
|
LIMIT 10
|
|
`, values);
|
|
|
|
// Daily trend (last 30 days)
|
|
const dailyTrend = await dbAll(`
|
|
SELECT
|
|
date(created_at) as date,
|
|
COUNT(*) as runs,
|
|
SUM(cost) as cost,
|
|
SUM(tokens_prompt + tokens_completion) as tokens
|
|
FROM monitoring_runs
|
|
${whereClause} ${whereClause ? 'AND' : 'WHERE'} created_at >= date('now', '-30 days')
|
|
GROUP BY date(created_at)
|
|
ORDER BY date ASC
|
|
`, values);
|
|
|
|
return {
|
|
summary: {
|
|
totalRuns: stats?.totalRuns || 0,
|
|
successfulRuns: stats?.successfulRuns || 0,
|
|
failedRuns: stats?.failedRuns || 0,
|
|
successRate: stats?.totalRuns ? ((stats.successfulRuns / stats.totalRuns) * 100).toFixed(1) : 0,
|
|
totalTokens: (stats?.totalPromptTokens || 0) + (stats?.totalCompletionTokens || 0),
|
|
totalPromptTokens: stats?.totalPromptTokens || 0,
|
|
totalCompletionTokens: stats?.totalCompletionTokens || 0,
|
|
totalCost: stats?.totalCost?.toFixed(4) || '0.0000',
|
|
avgDurationMs: Math.round(stats?.avgDurationMs || 0),
|
|
uniqueUsers: stats?.uniqueUsers || 0
|
|
},
|
|
byType,
|
|
byModel,
|
|
dailyTrend
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get top models by usage
|
|
*/
|
|
export async function getTopModels(options = {}) {
|
|
const { limit = 10, startDate, endDate } = options;
|
|
|
|
const conditions = ['model IS NOT NULL'];
|
|
const values = [];
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
values.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
values.push(endDate);
|
|
}
|
|
|
|
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
|
|
return dbAll(`
|
|
SELECT
|
|
model,
|
|
COUNT(*) as count,
|
|
SUM(tokens_prompt) as promptTokens,
|
|
SUM(tokens_completion) as completionTokens,
|
|
SUM(cost) as cost,
|
|
AVG(duration_ms) as avgDuration
|
|
FROM monitoring_runs
|
|
${whereClause}
|
|
GROUP BY model
|
|
ORDER BY count DESC
|
|
LIMIT ?
|
|
`, [...values, limit]);
|
|
}
|
|
|
|
/**
|
|
* Clean old data (retention policy)
|
|
*/
|
|
export async function cleanOldData(daysToKeep = 90) {
|
|
const cutoffDate = new Date();
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
const cutoff = cutoffDate.toISOString();
|
|
|
|
const result = await dbRun(`
|
|
DELETE FROM monitoring_events WHERE created_at < ?
|
|
`, [cutoff]);
|
|
|
|
const runsResult = await dbRun(`
|
|
DELETE FROM monitoring_runs WHERE created_at < ?
|
|
`, [cutoff]);
|
|
|
|
const feedbackResult = await dbRun(`
|
|
DELETE FROM monitoring_feedback WHERE created_at < ?
|
|
`, [cutoff]);
|
|
|
|
aiLogger.info({ daysToKeep }, 'Cleaned monitoring data');
|
|
return {
|
|
events: result?.changes || 0,
|
|
runs: runsResult?.changes || 0,
|
|
feedback: feedbackResult?.changes || 0
|
|
};
|
|
}
|