spinal-obs-node
Version:
WithSpinal cost-aware OpenTelemetry SDK for Node.js
553 lines (550 loc) • 31.2 kB
JavaScript
import {
Analytics
} from "../chunk-QPAMBULF.js";
import {
getConfig
} from "../chunk-7N3YU2IZ.js";
import {
estimateCost
} from "../chunk-GCDHWKI2.js";
// src/cli/index.ts
import { Command as Command2 } from "commander";
import Conf from "conf";
import open from "open";
import fs from "fs";
// src/cli/analytics.ts
import { Command } from "commander";
function createAnalyticsCommands() {
const commands = [];
const costCommand = new Command("cost").description("Analyze OpenAI API costs").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--by-model", "Show breakdown by model").option("--by-aggregation", "Show breakdown by aggregation ID").option("--trends", "Show cost trends").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const analysis = analytics.analyzeCosts({
since: options.since,
format: options.format,
byModel: options.byModel,
byAggregation: options.byAggregation,
trends: options.trends
});
if (options.format === "json") {
console.log(JSON.stringify(analysis, null, 2));
} else if (options.format === "csv") {
console.log("Period,Total Cost,Total Calls,Average Cost Per Call");
console.log(`${options.since},${analysis.totalCost.toFixed(6)},${analysis.totalCalls},${analysis.averageCostPerCall.toFixed(6)}`);
} else if (options.format === "summary") {
console.log(`\u{1F4B0} Total Cost: $${analysis.totalCost.toFixed(4)}`);
console.log(`\u{1F4CA} Total Calls: ${analysis.totalCalls}`);
console.log(`\u{1F4C8} Average Cost per Call: $${analysis.averageCostPerCall.toFixed(6)}`);
} else {
console.log(`
\u{1F4B0} Cost Analysis (Last ${options.since})
`);
console.log("\u2500".repeat(60));
console.log(`Total Cost: $${analysis.totalCost.toFixed(4)}`);
console.log(`Total API Calls: ${analysis.totalCalls}`);
console.log(`Average Cost per Call: $${analysis.averageCostPerCall.toFixed(6)}`);
console.log("\u2500".repeat(60));
if (options.byModel && Object.keys(analysis.costByModel).length > 0) {
console.log("\n\u{1F4CA} Cost by Model:");
Object.entries(analysis.costByModel).forEach(([model, data]) => {
console.log(`\u2022 ${model}: $${data.cost.toFixed(4)} (${data.calls} calls, ${data.percentage.toFixed(1)}%)`);
});
}
if (options.byAggregation && Object.keys(analysis.costByAggregation).length > 0) {
console.log("\n\u{1F3F7}\uFE0F Cost by Aggregation:");
Object.entries(analysis.costByAggregation).forEach(([agg, data]) => {
console.log(`\u2022 ${agg}: $${data.cost.toFixed(4)} (${data.calls} calls, ${data.percentage.toFixed(1)}%)`);
});
}
if (options.trends && analysis.costTrends.length > 0) {
console.log("\n\u{1F4C8} Cost Trends:");
analysis.costTrends.forEach((trend) => {
console.log(`\u2022 ${trend.date}: $${trend.cost.toFixed(4)} (${trend.calls} calls)`);
});
}
}
});
const usageCommand = new Command("usage").description("Analyze OpenAI API usage patterns").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "24h").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--tokens", "Show token breakdown").option("--by-model", "Show usage by model").option("--by-aggregation", "Show usage by aggregation ID").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const analysis = analytics.analyzeUsage({
since: options.since,
format: options.format,
byModel: options.byModel,
byAggregation: options.byAggregation
});
if (options.format === "json") {
console.log(JSON.stringify(analysis, null, 2));
} else if (options.format === "csv") {
console.log("Period,Total Calls,Total Tokens,Input Tokens,Output Tokens");
console.log(`${options.since},${analysis.totalCalls},${analysis.totalTokens},${analysis.inputTokens},${analysis.outputTokens}`);
} else if (options.format === "summary") {
console.log(`\u{1F4CA} Total Calls: ${analysis.totalCalls}`);
console.log(`\u{1F524} Total Tokens: ${analysis.totalTokens.toLocaleString()}`);
console.log(`\u{1F4E5} Input Tokens: ${analysis.inputTokens.toLocaleString()}`);
console.log(`\u{1F4E4} Output Tokens: ${analysis.outputTokens.toLocaleString()}`);
} else {
console.log(`
\u{1F4CA} Usage Analytics (Last ${options.since})
`);
console.log("\u2500".repeat(60));
console.log(`Total API Calls: ${analysis.totalCalls}`);
console.log(`Total Tokens: ${analysis.totalTokens.toLocaleString()}`);
console.log(`\u2022 Input tokens: ${analysis.inputTokens.toLocaleString()}`);
console.log(`\u2022 Output tokens: ${analysis.outputTokens.toLocaleString()}`);
console.log("\u2500".repeat(60));
if (options.tokens) {
console.log("\n\u{1F4C8} Token Efficiency:");
console.log(`\u2022 Average input tokens per call: ${analysis.tokenEfficiency.averageInputTokensPerCall.toFixed(1)}`);
console.log(`\u2022 Average output tokens per call: ${analysis.tokenEfficiency.averageOutputTokensPerCall.toFixed(1)}`);
console.log(`\u2022 Token ratio (output/input): ${analysis.tokenEfficiency.tokenRatio.toFixed(2)}`);
}
if (options.byModel && Object.keys(analysis.usageByModel).length > 0) {
console.log("\n\u{1F916} Usage by Model:");
Object.entries(analysis.usageByModel).forEach(([model, data]) => {
console.log(`\u2022 ${model}: ${data.calls} calls (${data.percentage.toFixed(1)}%), ${data.tokens.toLocaleString()} tokens`);
});
}
if (options.byAggregation && Object.keys(analysis.usageByAggregation).length > 0) {
console.log("\n\u{1F3F7}\uFE0F Usage by Aggregation:");
Object.entries(analysis.usageByAggregation).forEach(([agg, data]) => {
console.log(`\u2022 ${agg}: ${data.calls} calls (${data.percentage.toFixed(1)}%), ${data.tokens.toLocaleString()} tokens`);
});
}
}
});
const performanceCommand = new Command("performance").description("Analyze OpenAI API performance").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--response-times", "Show response time analysis").option("--errors", "Show error analysis").option("--by-model", "Show performance by model").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const analysis = analytics.analyzePerformance({
since: options.since,
format: options.format
});
if (options.format === "json") {
console.log(JSON.stringify(analysis, null, 2));
} else if (options.format === "csv") {
console.log("Period,Total Requests,Successful,Failed,Success Rate,Avg Response Time");
console.log(`${options.since},${analysis.totalRequests},${analysis.successful},${analysis.failed},${analysis.successRate.toFixed(1)}%,${analysis.responseTimes.average.toFixed(1)}ms`);
} else if (options.format === "summary") {
console.log(`\u26A1 Total Requests: ${analysis.totalRequests}`);
console.log(`\u2705 Successful: ${analysis.successful} (${analysis.successRate.toFixed(1)}%)`);
console.log(`\u274C Failed: ${analysis.failed}`);
console.log(`\u23F1\uFE0F Avg Response Time: ${analysis.responseTimes.average.toFixed(1)}ms`);
} else {
console.log(`
\u26A1 Performance Analytics (Last ${options.since})
`);
console.log("\u2500".repeat(60));
console.log(`Total Requests: ${analysis.totalRequests}`);
console.log(`Successful: ${analysis.successful} (${analysis.successRate.toFixed(1)}%)`);
console.log(`Failed: ${analysis.failed} (${(100 - analysis.successRate).toFixed(1)}%)`);
console.log("\u2500".repeat(60));
if (options.responseTimes) {
console.log("\n\u23F1\uFE0F Response Times:");
console.log(`\u2022 Average: ${analysis.responseTimes.average.toFixed(1)}ms`);
console.log(`\u2022 Median: ${analysis.responseTimes.median.toFixed(1)}ms`);
console.log(`\u2022 95th percentile: ${analysis.responseTimes.p95.toFixed(1)}ms`);
console.log(`\u2022 Fastest: ${analysis.responseTimes.fastest.toFixed(1)}ms`);
console.log(`\u2022 Slowest: ${analysis.responseTimes.slowest.toFixed(1)}ms`);
}
if (options.errors && analysis.errors.other > 0) {
console.log("\n\u{1F6A8} Error Analysis:");
console.log(`\u2022 Rate limit errors: ${analysis.errors.rateLimit}`);
console.log(`\u2022 Authentication errors: ${analysis.errors.authentication}`);
console.log(`\u2022 Network errors: ${analysis.errors.network}`);
console.log(`\u2022 Other errors: ${analysis.errors.other}`);
}
}
});
const modelsCommand = new Command("models").description("Compare performance and costs across different models").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "30d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--efficiency", "Show model efficiency metrics").option("--costs", "Show cost analysis by model").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const analysis = analytics.analyzeModels({
since: options.since,
format: options.format
});
if (options.format === "json") {
console.log(JSON.stringify(analysis, null, 2));
} else if (options.format === "summary") {
Object.entries(analysis.models).forEach(([model, data]) => {
console.log(`${model}: ${data.calls} calls, $${data.totalCost.toFixed(4)}, ${data.successRate.toFixed(1)}% success`);
});
} else {
console.log(`
\u{1F916} Model Analytics (Last ${options.since})
`);
console.log("\u2500".repeat(80));
Object.entries(analysis.models).forEach(([model, data]) => {
console.log(`${model}:`);
console.log(` \u2022 Calls: ${data.calls}`);
console.log(` \u2022 Total cost: $${data.totalCost.toFixed(4)}`);
console.log(` \u2022 Avg cost per call: $${data.avgCostPerCall.toFixed(6)}`);
console.log(` \u2022 Avg response time: ${data.avgResponseTime.toFixed(1)}ms`);
console.log(` \u2022 Success rate: ${data.successRate.toFixed(1)}%`);
console.log(` \u2022 Total tokens: ${data.totalTokens.toLocaleString()}`);
console.log("");
});
}
});
const aggregationsCommand = new Command("aggregations").description("Analyze usage by custom aggregation IDs").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--id <aggregationId>", "Show specific aggregation ID").option("--costs", "Show cost analysis by aggregation").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const analysis = analytics.analyzeAggregations({
since: options.since,
format: options.format
});
if (options.format === "json") {
console.log(JSON.stringify(analysis, null, 2));
} else if (options.format === "summary") {
Object.entries(analysis.aggregations).forEach(([agg, data]) => {
console.log(`${agg}: ${data.calls} calls, $${data.totalCost.toFixed(4)}, ${data.successRate.toFixed(1)}% success`);
});
} else {
console.log(`
\u{1F3F7}\uFE0F Aggregation Analytics (Last ${options.since})
`);
console.log("\u2500".repeat(80));
Object.entries(analysis.aggregations).forEach(([agg, data]) => {
console.log(`${agg}:`);
console.log(` \u2022 Calls: ${data.calls}`);
console.log(` \u2022 Total cost: $${data.totalCost.toFixed(4)}`);
console.log(` \u2022 Avg cost per call: $${data.avgCostPerCall.toFixed(6)}`);
console.log(` \u2022 Success rate: ${data.successRate.toFixed(1)}%`);
console.log(` \u2022 Total tokens: ${data.totalTokens.toLocaleString()}`);
console.log("");
});
}
});
const trendsCommand = new Command("trends").description("Identify usage patterns and trends over time").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "30d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--costs", "Show cost trends").option("--usage", "Show usage trends").option("--performance", "Show performance trends").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const analysis = analytics.analyzeTrends({
since: options.since,
format: options.format
});
if (options.format === "json") {
console.log(JSON.stringify(analysis, null, 2));
} else if (options.format === "summary") {
console.log(`\u{1F4CA} Daily avg calls: ${analysis.usageTrends.dailyAverageCalls.toFixed(1)}`);
console.log(`\u{1F4B0} Daily avg cost: $${analysis.costTrends.dailyAverageCost.toFixed(4)}`);
console.log(`\u26A1 Performance: ${analysis.performanceTrends.responseTimeTrend}`);
} else {
console.log(`
\u{1F4C8} Trends Analysis (Last ${options.since})
`);
console.log("\u2500".repeat(60));
if (options.usage || !options.costs && !options.performance) {
console.log("\n\u{1F4CA} Usage Trends:");
console.log(`\u2022 Daily average calls: ${analysis.usageTrends.dailyAverageCalls.toFixed(1)}`);
console.log(`\u2022 Peak usage: ${analysis.usageTrends.peakUsage.calls} calls (${analysis.usageTrends.peakUsage.date})`);
console.log(`\u2022 Growth rate: ${analysis.usageTrends.growthRate > 0 ? "+" : ""}${analysis.usageTrends.growthRate.toFixed(1)}% week-over-week`);
}
if (options.costs || !options.usage && !options.performance) {
console.log("\n\u{1F4B0} Cost Trends:");
console.log(`\u2022 Daily average cost: $${analysis.costTrends.dailyAverageCost.toFixed(4)}`);
console.log(`\u2022 Peak cost: $${analysis.costTrends.peakCost.cost.toFixed(4)} (${analysis.costTrends.peakCost.date})`);
console.log(`\u2022 Cost per call trend: ${analysis.costTrends.costPerCallTrend}`);
}
if (options.performance || !options.usage && !options.costs) {
console.log("\n\u26A1 Performance Trends:");
console.log(`\u2022 Response time trend: ${analysis.performanceTrends.responseTimeTrend}`);
console.log(`\u2022 Error rate trend: ${analysis.performanceTrends.errorRateTrend}`);
console.log(`\u2022 Success rate trend: ${analysis.performanceTrends.successRateTrend}`);
}
}
});
const optimizeCommand = new Command("optimize").description("Get actionable recommendations to optimize costs and performance").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--costs", "Show cost optimization recommendations").option("--performance", "Show performance optimization recommendations").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const recommendations = analytics.getOptimizationRecommendations({
since: options.since,
format: options.format
});
if (options.format === "json") {
console.log(JSON.stringify(recommendations, null, 2));
} else if (options.format === "summary") {
const allRecs = [
...recommendations.costOptimization,
...recommendations.performanceOptimization,
...recommendations.usageOptimization
];
allRecs.forEach((rec) => console.log(`\u2022 ${rec}`));
} else {
console.log(`
\u{1F4A1} Optimization Recommendations (Last ${options.since})
`);
console.log("\u2500".repeat(60));
if (options.costs || !options.performance && recommendations.costOptimization.length > 0) {
console.log("\n\u{1F4B0} Cost Optimization:");
recommendations.costOptimization.forEach((rec) => {
console.log(`\u2022 ${rec}`);
});
}
if (options.performance || !options.costs && recommendations.performanceOptimization.length > 0) {
console.log("\n\u26A1 Performance Optimization:");
recommendations.performanceOptimization.forEach((rec) => {
console.log(`\u2022 ${rec}`);
});
}
if (!options.costs && !options.performance && recommendations.usageOptimization.length > 0) {
console.log("\n\u{1F3AF} Usage Optimization:");
recommendations.usageOptimization.forEach((rec) => {
console.log(`\u2022 ${rec}`);
});
}
if (recommendations.costOptimization.length === 0 && recommendations.performanceOptimization.length === 0 && recommendations.usageOptimization.length === 0) {
console.log("\n\u2705 No specific optimization recommendations at this time.");
console.log("Your OpenAI API usage appears to be well-optimized!");
}
}
});
const responsesCommand = new Command("responses").description("Analyze OpenAI API response content and quality").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--errors", "Show detailed error analysis").option("--by-model", "Show response quality by model").option("--size-distribution", "Show response size distribution").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const analysis = analytics.analyzeResponses({
since: options.since,
format: options.format
});
if (options.format === "json") {
console.log(JSON.stringify(analysis, null, 2));
} else if (options.format === "csv") {
console.log("Period,Total Responses,Avg Size,Success Rate,Error Rate");
console.log(`${options.since},${analysis.totalResponses},${analysis.averageResponseSize.toFixed(1)},${analysis.errorAnalysis.successRate.toFixed(1)}%,${analysis.errorAnalysis.totalErrors}`);
} else if (options.format === "summary") {
console.log(`\u{1F4C4} Total Responses: ${analysis.totalResponses}`);
console.log(`\u{1F4CF} Avg Response Size: ${analysis.averageResponseSize.toFixed(1)} bytes`);
console.log(`\u2705 Success Rate: ${analysis.errorAnalysis.successRate.toFixed(1)}%`);
console.log(`\u274C Total Errors: ${analysis.errorAnalysis.totalErrors}`);
} else {
console.log(`
\u{1F4C4} Response Analysis (Last ${options.since})
`);
console.log("\u2500".repeat(60));
console.log(`Total Responses: ${analysis.totalResponses}`);
console.log(`Average Response Size: ${analysis.averageResponseSize.toFixed(1)} bytes`);
console.log(`Success Rate: ${analysis.errorAnalysis.successRate.toFixed(1)}%`);
console.log(`Error Rate: ${analysis.errorAnalysis.totalErrors > 0 ? (analysis.errorAnalysis.totalErrors / analysis.totalResponses * 100).toFixed(1) : 0}%`);
console.log("\u2500".repeat(60));
if (options.sizeDistribution) {
console.log("\n\u{1F4CA} Response Size Distribution:");
console.log(`\u2022 Small (< 500 bytes): ${analysis.responseSizeDistribution.small} responses`);
console.log(`\u2022 Medium (500-2000 bytes): ${analysis.responseSizeDistribution.medium} responses`);
console.log(`\u2022 Large (> 2000 bytes): ${analysis.responseSizeDistribution.large} responses`);
}
if (options.errors && analysis.errorAnalysis.totalErrors > 0) {
console.log("\n\u{1F6A8} Error Analysis:");
Object.entries(analysis.errorAnalysis.errorTypes).forEach(([errorType, count]) => {
console.log(`\u2022 ${errorType}: ${count} occurrences`);
});
if (analysis.errorAnalysis.errorMessages.length > 0) {
console.log("\n\u{1F4DD} Recent Error Messages:");
analysis.errorAnalysis.errorMessages.slice(0, 5).forEach((msg) => {
console.log(`\u2022 ${msg}`);
});
}
}
if (options.byModel && Object.keys(analysis.modelResponseQuality).length > 0) {
console.log("\n\u{1F916} Response Quality by Model:");
Object.entries(analysis.modelResponseQuality).forEach(([model, data]) => {
console.log(`${model}:`);
console.log(` \u2022 Avg response length: ${data.averageResponseLength.toFixed(1)} chars`);
console.log(` \u2022 Avg response size: ${data.averageResponseSize.toFixed(1)} bytes`);
console.log(` \u2022 Success rate: ${data.successRate.toFixed(1)}%`);
console.log("");
});
}
}
});
const contentCommand = new Command("content").description("Get insights about response content patterns and quality").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--patterns", "Show response length patterns").option("--finish-reasons", "Show finish reason distribution").option("--quality", "Show response quality metrics").action(async (options) => {
const config = getConfig();
const analytics = new Analytics(config.localStorePath);
const insights = analytics.getContentInsights({
since: options.since,
format: options.format
});
if (options.format === "json") {
console.log(JSON.stringify(insights, null, 2));
} else if (options.format === "summary") {
console.log(`\u{1F4CA} Response Patterns: ${insights.responsePatterns.shortResponses + insights.responsePatterns.mediumResponses + insights.responsePatterns.longResponses} total`);
console.log(`\u{1F3AF} Avg tokens/char: ${insights.responseQuality.averageTokensPerCharacter.toFixed(2)}`);
console.log(`\u26A1 Response efficiency: ${insights.responseQuality.responseEfficiency.toFixed(4)}`);
} else {
console.log(`
\u{1F4DD} Content Insights (Last ${options.since})
`);
console.log("\u2500".repeat(60));
if (options.patterns || !options.finishReasons && !options.quality) {
console.log("\n\u{1F4CA} Response Length Patterns:");
const total = insights.responsePatterns.shortResponses + insights.responsePatterns.mediumResponses + insights.responsePatterns.longResponses;
console.log(`\u2022 Short responses (< 50 chars): ${insights.responsePatterns.shortResponses} (${total > 0 ? (insights.responsePatterns.shortResponses / total * 100).toFixed(1) : 0}%)`);
console.log(`\u2022 Medium responses (50-200 chars): ${insights.responsePatterns.mediumResponses} (${total > 0 ? (insights.responsePatterns.mediumResponses / total * 100).toFixed(1) : 0}%)`);
console.log(`\u2022 Long responses (> 200 chars): ${insights.responsePatterns.longResponses} (${total > 0 ? (insights.responsePatterns.longResponses / total * 100).toFixed(1) : 0}%)`);
}
if (options.finishReasons || !options.patterns && !options.quality) {
console.log("\n\u{1F3AF} Finish Reasons:");
Object.entries(insights.finishReasons).forEach(([reason, count]) => {
console.log(`\u2022 ${reason}: ${count} responses`);
});
}
if (options.quality || !options.patterns && !options.finishReasons) {
console.log("\n\u26A1 Response Quality Metrics:");
console.log(`\u2022 Average tokens per character: ${insights.responseQuality.averageTokensPerCharacter.toFixed(2)}`);
console.log(`\u2022 Response efficiency (tokens/byte): ${insights.responseQuality.responseEfficiency.toFixed(4)}`);
}
if (Object.values(insights.commonErrors).some((count) => count > 0)) {
console.log("\n\u{1F6A8} Common Error Types:");
Object.entries(insights.commonErrors).forEach(([errorType, count]) => {
if (count > 0) {
console.log(`\u2022 ${errorType}: ${count} occurrences`);
}
});
}
}
});
commands.push(
costCommand,
usageCommand,
performanceCommand,
modelsCommand,
aggregationsCommand,
trendsCommand,
optimizeCommand,
responsesCommand,
contentCommand
);
return commands;
}
// src/cli/index.ts
var program = new Command2();
program.name("spinal").description("Spinal CLI").version("0.1.0");
var store = new Conf({ projectName: "spinal" });
program.command("status").description("Show current mode and configuration summary").action(async () => {
const cfg = getConfig();
const mode = cfg.mode;
const endpoint = cfg.endpoint;
const excluded = process.env.SPINAL_EXCLUDED_HOSTS ?? "api.anthropic.com,api.azure.com";
const excludeOpenAI = process.env.SPINAL_EXCLUDE_OPENAI === "true";
console.log(JSON.stringify({ mode, endpoint, localStorePath: cfg.localStorePath, excludedHosts: excluded, excludeOpenAI }, null, 2));
});
program.command("login").description("Login for cloud mode (opens backend dashboard)").option("--dashboard-url <url>", "Backend dashboard URL", "https://dashboard.withspinal.com/login").action(async (opts) => {
await open(opts.dashboardUrl);
console.log("Opened browser for login. After obtaining an API key, set SPINAL_API_KEY.");
});
program.command("init").description("Initialize configuration (optional)").option("--endpoint <url>", "Spinal endpoint", process.env.SPINAL_TRACING_ENDPOINT || "https://cloud.withspinal.com").action(async (opts) => {
store.set("endpoint", opts.endpoint);
console.log("Saved endpoint to local config.");
});
program.command("local").description("Display local collected data in a readable format").option("--limit <number>", "Limit number of spans to display", "10").option("--format <format>", "Output format: table, json, or summary", "table").action(async (opts) => {
const cfg = getConfig();
const file = cfg.localStorePath;
if (!fs.existsSync(file)) {
console.log("No local data found. Start your application with Spinal configured to collect data.");
return;
}
const raw = await fs.promises.readFile(file, "utf8");
const lines = raw.trim().length ? raw.trim().split("\n") : [];
if (lines.length === 0) {
console.log("No spans collected yet. Start your application with Spinal configured to collect data.");
return;
}
const spans = [];
for (const line of lines) {
try {
const span = JSON.parse(line);
spans.push(span);
} catch {
}
}
const limit = parseInt(opts.limit, 10);
const displaySpans = spans.slice(-limit);
if (opts.format === "json") {
console.log(JSON.stringify(displaySpans, null, 2));
} else if (opts.format === "summary") {
const summary = {
totalSpans: spans.length,
uniqueTraces: new Set(spans.map((s) => s.trace_id)).size,
spanTypes: spans.reduce((acc, span) => {
const type = span.name || "unknown";
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {}),
estimatedCost: spans.reduce((total, span) => {
const attrs = span.attributes || {};
const inputTokens = Number(attrs["spinal.input_tokens"] || 0);
const outputTokens = Number(attrs["spinal.output_tokens"] || 0);
const model = String(attrs["spinal.model"] || "openai:gpt-4o-mini");
return total + estimateCost({ model, inputTokens, outputTokens });
}, 0)
};
console.log(JSON.stringify(summary, null, 2));
} else {
console.log(`
\u{1F4CA} Spinal Local Data (showing last ${displaySpans.length} of ${spans.length} spans)
`);
console.log("\u2500".repeat(120));
console.log(`${"Name".padEnd(30)} ${"Trace ID".padEnd(32)} ${"Duration (ms)".padEnd(12)} ${"Status".padEnd(8)} ${"Model".padEnd(15)} ${"Cost ($)".padEnd(8)}`);
console.log("\u2500".repeat(120));
for (const span of displaySpans) {
const name = (span.name || "unknown").substring(0, 29).padEnd(30);
const traceId = span.trace_id.substring(0, 31).padEnd(32);
const duration = span.end_time && span.start_time ? ((span.end_time - span.start_time) / 1e6).toFixed(1).padEnd(12) : "N/A".padEnd(12);
const status = String(span.status?.code || "UNSET").padEnd(8);
const attrs = span.attributes || {};
const model = (attrs["spinal.model"] || "N/A").toString().substring(0, 14).padEnd(15);
const inputTokens = Number(attrs["spinal.input_tokens"] || 0);
const outputTokens = Number(attrs["spinal.output_tokens"] || 0);
const cost = inputTokens > 0 || outputTokens > 0 ? estimateCost({
model: String(attrs["spinal.model"] || "openai:gpt-4o-mini"),
inputTokens,
outputTokens
}).toFixed(4).padEnd(8) : "N/A".padEnd(8);
console.log(`${name} ${traceId} ${duration} ${status} ${model} ${cost}`);
}
console.log("\u2500".repeat(120));
const totalCost = spans.reduce((total, span) => {
const attrs = span.attributes || {};
const inputTokens = Number(attrs["spinal.input_tokens"] || 0);
const outputTokens = Number(attrs["spinal.output_tokens"] || 0);
const model = String(attrs["spinal.model"] || "openai:gpt-4o-mini");
return total + estimateCost({ model, inputTokens, outputTokens });
}, 0);
console.log(`
\u{1F4B0} Total estimated cost: $${totalCost.toFixed(4)}`);
console.log(`\u{1F4C8} Total spans collected: ${spans.length}`);
console.log(`\u{1F50D} Unique traces: ${new Set(spans.map((s) => s.trace_id)).size}`);
}
});
program.command("report").description("Summarize local usage and estimated costs").option("--since <duration>", "Time window, e.g., 24h", "24h").action(async () => {
const cfg = getConfig();
if (cfg.mode !== "local") {
console.log("report is for local mode. Set SPINAL_MODE=local or omit SPINAL_API_KEY.");
return;
}
const file = cfg.localStorePath;
if (!fs.existsSync(file)) {
console.log("No local data found.");
return;
}
const raw = await fs.promises.readFile(file, "utf8");
const lines = raw.trim().length ? raw.trim().split("\n") : [];
let count = 0;
let est = 0;
for (const line of lines) {
try {
const span = JSON.parse(line);
const attrs = span.attributes || {};
const inputTokens = Number(attrs["spinal.input_tokens"] || 0);
const outputTokens = Number(attrs["spinal.output_tokens"] || 0);
const model = String(attrs["spinal.model"] || "openai:gpt-4o-mini");
est += estimateCost({ model, inputTokens, outputTokens });
count++;
} catch {
}
}
console.log(JSON.stringify({ spansProcessed: count, estimatedCostUSD: Number(est.toFixed(4)) }, null, 2));
});
var analyticsCommands = createAnalyticsCommands();
analyticsCommands.forEach((cmd) => program.addCommand(cmd));
program.parseAsync();
//# sourceMappingURL=index.js.map