import { PrometheusClient } from './prometheus-client.js';
import {
UsageData,
UsageSummary,
UsageTrend,
SessionAnalytics,
ToolUsageBreakdown,
UsageLimits,
UsageWarnings
} from './types.js';
export class TelemetryService {
private prometheus: PrometheusClient;
private sessionStartTime: Date;
constructor(prometheusUrl?: string) {
this.prometheus = new PrometheusClient(prometheusUrl);
this.sessionStartTime = new Date(); // Approximate session start
}
private getStartOfDay(): Date {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
}
private getStartOfWeek(): Date {
const now = new Date();
const dayOfWeek = now.getDay();
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Monday as start of week
return new Date(now.getFullYear(), now.getMonth(), diff);
}
async getCurrentSessionUsage(): Promise<UsageData> {
const sessionStart = this.sessionStartTime.toISOString();
const now = new Date().toISOString();
// Query for usage since session start - use sum() to aggregate across all label dimensions
const queries = {
tokens: `sum(increase(claude_code_token_usage_tokens_total[${this.getTimeRange()}]))`,
cost: `sum(increase(claude_code_cost_usage_USD_total[${this.getTimeRange()}]))`,
sessions: `sum(increase(claude_code_session_count_total[${this.getTimeRange()}]))`,
activeTime: `sum(increase(claude_code_active_time_seconds_total[${this.getTimeRange()}]))`,
linesOfCode: `sum(increase(claude_code_lines_of_code_count_total[${this.getTimeRange()}]))`,
commits: `sum(increase(claude_code_commit_count_total[${this.getTimeRange()}]))`,
editDecisions: `sum(increase(claude_code_code_edit_tool_decision_total[${this.getTimeRange()}]))`
};
const results = await this.executeQueries(queries);
return this.buildUsageData(results);
}
async getTodayUsage(): Promise<UsageData> {
const startOfDay = this.getStartOfDay();
const now = new Date();
const queries = {
tokens: `sum(increase(claude_code_token_usage_tokens_total[1d]))`,
cost: `sum(increase(claude_code_cost_usage_USD_total[1d]))`,
sessions: `sum(increase(claude_code_session_count_total[1d]))`,
activeTime: `sum(increase(claude_code_active_time_seconds_total[1d]))`,
linesOfCode: `sum(increase(claude_code_lines_of_code_count_total[1d]))`,
commits: `sum(increase(claude_code_commit_count_total[1d]))`,
editDecisions: `sum(increase(claude_code_code_edit_tool_decision_total[1d]))`
};
const results = await this.executeQueries(queries);
return this.buildUsageData(results);
}
async getWeekUsage(): Promise<UsageData> {
const queries = {
tokens: `sum(increase(claude_code_token_usage_tokens_total[7d]))`,
cost: `sum(increase(claude_code_cost_usage_USD_total[7d]))`,
sessions: `sum(increase(claude_code_session_count_total[7d]))`,
activeTime: `sum(increase(claude_code_active_time_seconds_total[7d]))`,
linesOfCode: `sum(increase(claude_code_lines_of_code_count_total[7d]))`,
commits: `sum(increase(claude_code_commit_count_total[7d]))`,
editDecisions: `sum(increase(claude_code_code_edit_tool_decision_total[7d]))`
};
const results = await this.executeQueries(queries);
return this.buildUsageData(results);
}
async getUsageSummary(): Promise<UsageSummary> {
const [currentSession, today, thisWeek] = await Promise.all([
this.getCurrentSessionUsage(),
this.getTodayUsage(),
this.getWeekUsage()
]);
return {
currentSession,
today,
thisWeek,
period: {
start: this.getStartOfWeek().toISOString(),
end: new Date().toISOString()
}
};
}
async getUsageTrends(daysBack: number = 7): Promise<UsageTrend[]> {
const endTime = new Date();
const startTime = new Date(endTime.getTime() - (daysBack * 24 * 60 * 60 * 1000));
const [tokensResult, costResult, sessionsResult] = await Promise.all([
this.prometheus.queryRange('claude_code_token_usage_tokens_total', startTime, endTime, '1h'),
this.prometheus.queryRange('claude_code_cost_usage_USD_total', startTime, endTime, '1h'),
this.prometheus.queryRange('claude_code_session_count_total', startTime, endTime, '1h')
]);
const tokensSeries = this.prometheus.getTimeSeries(tokensResult);
const costSeries = this.prometheus.getTimeSeries(costResult);
const sessionsSeries = this.prometheus.getTimeSeries(sessionsResult);
// Combine the series data
const trends: UsageTrend[] = [];
for (let i = 0; i < tokensSeries.length; i++) {
trends.push({
timestamp: tokensSeries[i].timestamp.toISOString(),
tokens: tokensSeries[i].value,
cost: costSeries[i]?.value || 0,
sessions: sessionsSeries[i]?.value || 0
});
}
return trends;
}
async getSessionAnalytics(): Promise<SessionAnalytics> {
const queries = {
totalSessions: 'sum(claude_code_session_count_total)',
totalTokens: 'sum(claude_code_token_usage_tokens_total)',
totalActiveTime: 'sum(claude_code_active_time_seconds_total)',
totalCost: 'sum(claude_code_cost_usage_USD_total)',
totalLines: 'sum(claude_code_lines_of_code_count_total)',
totalCommits: 'sum(claude_code_commit_count_total)'
};
const results = await this.executeQueries(queries);
const totalSessions = results.totalSessions || 1; // Avoid division by zero
return {
totalSessions,
averageTokensPerSession: (results.totalTokens || 0) / totalSessions,
averageActiveTimePerSession: (results.totalActiveTime || 0) / totalSessions,
averageCostPerSession: (results.totalCost || 0) / totalSessions,
longestSession: {
tokens: results.totalTokens || 0, // This would need more sophisticated querying for actual longest session
activeTime: results.totalActiveTime || 0
},
mostProductiveSession: {
linesOfCode: results.totalLines || 0,
commits: results.totalCommits || 0
}
};
}
async getToolUsageBreakdown(): Promise<ToolUsageBreakdown> {
const queries = {
totalEditDecisions: 'sum(claude_code_code_edit_tool_decision_total)',
totalSessions: 'sum(claude_code_session_count_total)'
};
const results = await this.executeQueries(queries);
const totalSessions = results.totalSessions || 1;
return {
totalEditDecisions: results.totalEditDecisions || 0,
averageDecisionsPerSession: (results.totalEditDecisions || 0) / totalSessions,
peakDecisionHour: 14 // Placeholder - would need hourly breakdown analysis
};
}
async checkUsageLimits(limits: UsageLimits): Promise<UsageWarnings> {
const summary = await this.getUsageSummary();
const warnings: string[] = [];
const percentages: any = {};
// Check daily limits
if (limits.dailyTokenLimit) {
const dailyPercent = (summary.today.tokens / limits.dailyTokenLimit) * 100;
percentages.dailyTokens = dailyPercent;
if (dailyPercent > 90) {
warnings.push(`Daily token usage at ${dailyPercent.toFixed(1)}% of limit`);
} else if (dailyPercent > 80) {
warnings.push(`Daily token usage approaching limit (${dailyPercent.toFixed(1)}%)`);
}
}
// Check weekly limits
if (limits.weeklyTokenLimit) {
const weeklyPercent = (summary.thisWeek.tokens / limits.weeklyTokenLimit) * 100;
percentages.weeklyTokens = weeklyPercent;
if (weeklyPercent > 90) {
warnings.push(`Weekly token usage at ${weeklyPercent.toFixed(1)}% of limit`);
} else if (weeklyPercent > 80) {
warnings.push(`Weekly token usage approaching limit (${weeklyPercent.toFixed(1)}%)`);
}
}
// Check session limits
if (limits.sessionTokenLimit) {
const sessionPercent = (summary.currentSession.tokens / limits.sessionTokenLimit) * 100;
percentages.sessionTokens = sessionPercent;
if (sessionPercent > 90) {
warnings.push(`Session token usage at ${sessionPercent.toFixed(1)}% of limit`);
} else if (sessionPercent > 80) {
warnings.push(`Session token usage approaching limit (${sessionPercent.toFixed(1)}%)`);
}
}
// Similar checks for cost limits...
if (limits.dailyCostLimit) {
const dailyCostPercent = (summary.today.cost / limits.dailyCostLimit) * 100;
percentages.dailyCost = dailyCostPercent;
if (dailyCostPercent > 80) {
warnings.push(`Daily cost approaching limit (${dailyCostPercent.toFixed(1)}%)`);
}
}
return {
warnings,
usage: {
current: summary.today,
limits,
percentages
}
};
}
private async executeQueries(queries: Record<string, string>): Promise<Record<string, number>> {
const results: Record<string, number> = {};
for (const [key, query] of Object.entries(queries)) {
try {
const result = await this.prometheus.query(query);
results[key] = this.prometheus.getLatestValue(result);
} catch (error) {
console.warn(`Query failed for ${key}:`, error);
results[key] = 0;
}
}
return results;
}
private buildUsageData(results: Record<string, number>): UsageData {
return {
tokens: results.tokens || 0,
cost: results.cost || 0,
sessions: results.sessions || 0,
activeTime: results.activeTime || 0,
linesOfCode: results.linesOfCode || 0,
commits: results.commits || 0,
editDecisions: results.editDecisions || 0
};
}
private getTimeRange(): string {
const now = new Date();
const diffMs = now.getTime() - this.sessionStartTime.getTime();
const diffMinutes = Math.max(1, Math.floor(diffMs / (1000 * 60)));
// Use a minimum of 30 minutes to capture session data even if MCP server just started
// This helps when the MCP server restarts but the Claude session continues
return `${Math.max(30, diffMinutes)}m`;
}
async isHealthy(): Promise<boolean> {
return await this.prometheus.isHealthy();
}
}