UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

689 lines 32.2 kB
#!/usr/bin/env node /* eslint-disable no-console, curly */ /** * NeuroLink CLI Telemetry Commands * * Commands for managing telemetry and observability exporters: * - status: Show exporter status * - configure: Configure an exporter * - list-exporters: List configured exporters * - flush: Flush pending spans * - stats: Show token/cost stats */ import chalk from "chalk"; import ora from "ora"; import { logger } from "../../lib/utils/logger.js"; import { NeuroLink } from "../../lib/neurolink.js"; import { flushOpenTelemetry } from "../../lib/services/server/ai/observability/instrumentation.js"; import { formatRow, formatCost } from "../utils/formatters.js"; /** * Available exporter names */ const AVAILABLE_EXPORTERS = [ "langfuse", "langsmith", "otel", "datadog", "sentry", "braintrust", "arize", "posthog", "laminar", ]; /** * Telemetry Command Factory */ export class TelemetryCommandFactory { /** * Create the telemetry command group */ static createTelemetryCommands() { return { command: "telemetry <subcommand>", aliases: ["tel"], describe: "Telemetry and exporter management", builder: (yargs) => { return yargs .command(TelemetryCommandFactory.createStatusCommand()) .command(TelemetryCommandFactory.createConfigureCommand()) .command(TelemetryCommandFactory.createListExportersCommand()) .command(TelemetryCommandFactory.createFlushCommand()) .command(TelemetryCommandFactory.createStatsCommand()) .demandCommand(1, "Please specify a subcommand") .strict(); }, handler: () => { // This handler is not called directly due to demandCommand }, }; } /** * Create the status subcommand */ static createStatusCommand() { return { command: "status", describe: "Show exporter status and health", builder: (yargs) => { return yargs .option("format", { alias: "f", type: "string", choices: ["text", "json", "table"], default: "text", describe: "Output format", }) .option("quiet", { alias: "q", type: "boolean", default: false, describe: "Minimal output", }); }, handler: async (args) => { const spinner = args.quiet ? null : ora("Checking exporter status...").start(); try { const neurolink = new NeuroLink(); const status = neurolink.getTelemetryStatus(); if (spinner) spinner.succeed("Status retrieved"); if (args.format === "json") { console.log(JSON.stringify(status, null, 2)); } else { console.log(""); console.log(chalk.bold.cyan("=== Telemetry Status ===")); console.log(""); // Telemetry enabled status const enabledIcon = status.enabled ? chalk.green("ENABLED") : chalk.red("DISABLED"); console.log(formatRow("Telemetry:", enabledIcon)); // OpenTelemetry status if (status.openTelemetry) { console.log(""); console.log(chalk.bold("OpenTelemetry:")); const otelStatus = status.openTelemetry.enabled ? chalk.green("Active") : chalk.gray("Inactive"); console.log(formatRow(" Status:", otelStatus)); if (status.openTelemetry.endpoint) { console.log(formatRow(" Endpoint:", status.openTelemetry.endpoint)); } if (status.openTelemetry.serviceName) { console.log(formatRow(" Service:", status.openTelemetry.serviceName)); } } // Langfuse status if (status.langfuse) { console.log(""); console.log(chalk.bold("Langfuse:")); const lfStatus = status.langfuse.enabled ? chalk.green("Active") : chalk.gray("Inactive"); console.log(formatRow(" Status:", lfStatus)); if (status.langfuse.baseUrl) { console.log(formatRow(" URL:", status.langfuse.baseUrl)); } if (status.langfuse.environment) { console.log(formatRow(" Environment:", status.langfuse.environment)); } } // Exporters health summary if (status.exporters && status.exporters.length > 0) { console.log(""); console.log(chalk.bold("Exporter Health:")); for (const exporter of status.exporters) { const healthIcon = exporter.healthy ? chalk.green("[OK]") : chalk.red("[ERROR]"); const pendingInfo = exporter.pendingSpans ? chalk.gray(` (${exporter.pendingSpans} pending)`) : ""; console.log(` ${healthIcon} ${exporter.name}${pendingInfo}`); if (exporter.errors && exporter.errors.length > 0) { for (const error of exporter.errors.slice(0, 2)) { console.log(chalk.red(` Error: ${error}`)); } } } } else { console.log(""); console.log(chalk.gray("No exporters configured.")); } console.log(""); } } catch (error) { if (spinner) spinner.fail("Failed to get status"); logger.error("Error:", error instanceof Error ? error.message : String(error)); process.exit(1); } }, }; } /** * Create the configure subcommand */ static createConfigureCommand() { return { command: "configure", describe: "Configure an exporter with JSON settings", builder: (yargs) => { return yargs .option("exporter", { alias: "e", type: "string", demandOption: true, choices: AVAILABLE_EXPORTERS, describe: "Exporter name to configure", }) .option("config", { alias: "c", type: "string", demandOption: true, describe: "JSON configuration string", }) .option("format", { alias: "f", type: "string", choices: ["text", "json", "table"], default: "text", describe: "Output format", }) .option("quiet", { alias: "q", type: "boolean", default: false, describe: "Minimal output", }); }, handler: async (args) => { const spinner = args.quiet ? null : ora(`Configuring ${args.exporter} exporter...`).start(); try { // Parse the JSON config let config; try { config = JSON.parse(args.config); } catch { if (spinner) spinner.fail("Invalid JSON configuration"); console.log(chalk.red("Error: Configuration must be valid JSON")); console.log(""); console.log("Example:"); console.log(chalk.gray(` neurolink telemetry configure --exporter langfuse --config '{"publicKey":"pk-...", "secretKey":"sk-..."}'`)); process.exit(1); } // Validate required fields based on exporter type const validationResult = validateExporterConfig(args.exporter, config); if (!validationResult.valid) { if (spinner) spinner.fail("Configuration validation failed"); console.log(chalk.red(`Error: ${validationResult.error}`)); console.log(""); console.log(chalk.yellow(`Required fields for ${args.exporter}:`)); for (const field of validationResult.requiredFields ?? []) { console.log(chalk.gray(` - ${field}`)); } process.exit(1); } // Currently, exporter configuration is done via environment variables // or SDK initialization. This command provides guidance on how to configure. if (spinner) spinner.succeed(`${args.exporter} configuration validated`); if (args.format === "json") { console.log(JSON.stringify({ exporter: args.exporter, config: config, valid: true, message: "Configuration validated. Set environment variables or use SDK initialization.", }, null, 2)); } else { console.log(""); console.log(chalk.bold.cyan(`=== ${args.exporter} Configuration ===`)); console.log(""); console.log(chalk.green("Configuration validated successfully!")); console.log(""); console.log(chalk.bold("To apply this configuration:")); console.log(""); // Show environment variable instructions const envVars = getExporterEnvVars(args.exporter); console.log(chalk.yellow("Option 1: Set environment variables")); for (const [key, description] of Object.entries(envVars)) { console.log(chalk.gray(` export ${key}="<${description}>"`)); } console.log(""); console.log(chalk.yellow("Option 2: SDK initialization")); console.log(chalk.gray(` const neurolink = new NeuroLink({`)); console.log(chalk.gray(` observability: {`)); console.log(chalk.gray(` ${args.exporter}: ${JSON.stringify(config, null, 6).split("\n").join("\n ")}`)); console.log(chalk.gray(` }`)); console.log(chalk.gray(` });`)); console.log(""); } } catch (error) { if (spinner) spinner.fail("Failed to configure exporter"); logger.error("Error:", error instanceof Error ? error.message : String(error)); process.exit(1); } }, }; } /** * Create the list-exporters subcommand */ static createListExportersCommand() { return { command: "list-exporters", aliases: ["list", "ls"], describe: "List all available and configured exporters", builder: (yargs) => { return yargs .option("format", { alias: "f", type: "string", choices: ["text", "json", "table"], default: "text", describe: "Output format", }) .option("quiet", { alias: "q", type: "boolean", default: false, describe: "Minimal output", }); }, handler: async (args) => { const spinner = args.quiet ? null : ora("Listing exporters...").start(); try { const neurolink = new NeuroLink(); const status = neurolink.getTelemetryStatus(); if (spinner) spinner.succeed("Exporters listed"); const configuredExporters = status.exporters ?? []; const configuredNames = new Set(configuredExporters.map((e) => e.name.toLowerCase())); if (args.format === "json") { console.log(JSON.stringify({ available: AVAILABLE_EXPORTERS, configured: configuredExporters, }, null, 2)); } else { console.log(""); console.log(chalk.bold.cyan("=== Available Exporters ===")); console.log(""); for (const exporter of AVAILABLE_EXPORTERS) { const isConfigured = configuredNames.has(exporter); const configuredExporter = configuredExporters.find((e) => e.name.toLowerCase() === exporter); const statusIcon = isConfigured ? configuredExporter?.healthy ? chalk.green("[ACTIVE]") : chalk.yellow("[CONFIGURED]") : chalk.gray("[AVAILABLE]"); const description = getExporterDescription(exporter); console.log(`${statusIcon} ${chalk.bold(exporter)}`); console.log(chalk.gray(` ${description}`)); if (isConfigured && configuredExporter) { if (configuredExporter.pendingSpans) { console.log(chalk.gray(` Pending spans: ${configuredExporter.pendingSpans}`)); } if (configuredExporter.lastExportTime) { const lastExport = new Date(configuredExporter.lastExportTime); console.log(chalk.gray(` Last export: ${lastExport.toISOString()}`)); } } console.log(""); } console.log(chalk.bold("Configuration Help:")); console.log(chalk.gray(" Use 'neurolink telemetry configure --exporter <name> --config <json>' to configure an exporter")); console.log(""); } } catch (error) { if (spinner) spinner.fail("Failed to list exporters"); logger.error("Error:", error instanceof Error ? error.message : String(error)); process.exit(1); } }, }; } /** * Create the flush subcommand */ static createFlushCommand() { return { command: "flush", describe: "Flush all pending spans to exporters", builder: (yargs) => { return yargs .option("timeout", { alias: "t", type: "number", default: 30000, describe: "Timeout in milliseconds", }) .option("format", { alias: "f", type: "string", choices: ["text", "json", "table"], default: "text", describe: "Output format", }) .option("quiet", { alias: "q", type: "boolean", default: false, describe: "Minimal output", }); }, handler: async (args) => { const spinner = args.quiet ? null : ora("Flushing pending spans...").start(); try { const neurolink = new NeuroLink(); const statusBefore = neurolink.getTelemetryStatus(); // Count pending spans before flush const pendingBefore = statusBefore.exporters?.reduce((sum, e) => sum + (e.pendingSpans ?? 0), 0) ?? 0; // Create a timeout promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("Flush operation timed out")), args.timeout ?? 30000); }); // Flush OpenTelemetry spans const flushPromise = flushOpenTelemetry(); // Race between flush and timeout await Promise.race([flushPromise, timeoutPromise]); // Get status after flush const statusAfter = neurolink.getTelemetryStatus(); const pendingAfter = statusAfter.exporters?.reduce((sum, e) => sum + (e.pendingSpans ?? 0), 0) ?? 0; const flushedCount = Math.max(0, pendingBefore - pendingAfter); if (spinner) spinner.succeed("Flush completed"); if (args.format === "json") { console.log(JSON.stringify({ success: true, pendingBefore, pendingAfter, flushed: flushedCount, }, null, 2)); } else { console.log(""); console.log(chalk.bold.cyan("=== Flush Complete ===")); console.log(""); console.log(formatRow("Spans before:", pendingBefore.toString())); console.log(formatRow("Spans after:", pendingAfter.toString())); console.log(formatRow("Flushed:", chalk.green(flushedCount.toString()))); console.log(""); } } catch (error) { if (spinner) spinner.fail("Failed to flush spans"); logger.error("Error:", error instanceof Error ? error.message : String(error)); process.exit(1); } }, }; } /** * Create the stats subcommand */ static createStatsCommand() { return { command: "stats", describe: "Show token usage and cost statistics", builder: (yargs) => { return yargs .option("format", { alias: "f", type: "string", choices: ["text", "json", "table"], default: "text", describe: "Output format", }) .option("detailed", { alias: "d", type: "boolean", default: false, describe: "Show detailed statistics", }) .option("by-model", { alias: "m", type: "boolean", default: true, describe: "Show breakdown by model", }) .option("by-provider", { alias: "p", type: "boolean", default: true, describe: "Show breakdown by provider", }) .option("quiet", { alias: "q", type: "boolean", default: false, describe: "Minimal output", }); }, handler: async (args) => { const spinner = args.quiet ? null : ora("Gathering statistics...").start(); try { const neurolink = new NeuroLink(); const metrics = neurolink.getMetrics(); if (spinner) spinner.succeed("Statistics retrieved"); if (args.format === "json") { console.log(JSON.stringify({ tokens: { input: metrics.tokens.totalInputTokens, output: metrics.tokens.totalOutputTokens, total: metrics.tokens.totalTokens, cacheRead: metrics.tokens.cacheReadTokens, reasoning: metrics.tokens.reasoningTokens, }, cost: { total: metrics.totalCost, byProvider: metrics.costByProvider, byModel: metrics.costByModel, }, requests: { total: metrics.totalSpans, successful: metrics.successfulSpans, failed: metrics.failedSpans, successRate: metrics.successRate, }, latency: metrics.latency, }, null, 2)); } else { console.log(""); console.log(chalk.bold.cyan("=== Token & Cost Statistics ===")); console.log(""); // Token usage console.log(chalk.bold("Token Usage:")); console.log(formatRow(" Input tokens:", metrics.tokens.totalInputTokens.toLocaleString())); console.log(formatRow(" Output tokens:", metrics.tokens.totalOutputTokens.toLocaleString())); console.log(formatRow(" Total tokens:", metrics.tokens.totalTokens.toLocaleString())); if (args.detailed) { if (metrics.tokens.cacheReadTokens > 0) { console.log(formatRow(" Cache read:", metrics.tokens.cacheReadTokens.toLocaleString())); } if (metrics.tokens.reasoningTokens > 0) { console.log(formatRow(" Reasoning:", metrics.tokens.reasoningTokens.toLocaleString())); } } // Cost summary console.log(""); console.log(chalk.bold("Cost Summary:")); console.log(formatRow(" Total cost:", formatCost(metrics.totalCost ?? 0))); // Cost by provider if (args.byProvider !== false && metrics.costByProvider && metrics.costByProvider.length > 0) { console.log(""); console.log(chalk.bold("Cost by Provider:")); const sortedProviders = [...metrics.costByProvider].sort((a, b) => b.totalCost - a.totalCost); for (const provider of sortedProviders) { console.log(` ${chalk.cyan(provider.provider.padEnd(15))} ${formatCost(provider.totalCost)}`); console.log(chalk.gray(` ${provider.requestCount} requests, avg ${formatCost(provider.avgCostPerRequest)}/req`)); } } // Cost by model if (args.byModel !== false && metrics.costByModel && metrics.costByModel.length > 0) { console.log(""); console.log(chalk.bold("Cost by Model:")); const sortedModels = [...metrics.costByModel].sort((a, b) => b.totalCost - a.totalCost); for (const model of sortedModels) { console.log(` ${chalk.cyan(model.model)}`); console.log(` Cost: ${formatCost(model.totalCost)}`); console.log(chalk.gray(` ${model.requestCount} requests, avg ${formatCost(model.avgCostPerRequest)}/req`)); if (args.detailed) { console.log(chalk.gray(` ${model.inputTokens.toLocaleString()} input, ${model.outputTokens.toLocaleString()} output tokens`)); } } } // Request statistics console.log(""); console.log(chalk.bold("Request Statistics:")); console.log(formatRow(" Total requests:", metrics.totalSpans.toLocaleString())); console.log(formatRow(" Successful:", metrics.successfulSpans.toLocaleString())); console.log(formatRow(" Failed:", metrics.failedSpans.toLocaleString())); console.log(formatRow(" Success rate:", `${(metrics.successRate * 100).toFixed(2)}%`)); // Latency (if detailed) if (args.detailed && metrics.latency.count > 0) { console.log(""); console.log(chalk.bold("Latency (ms):")); console.log(formatRow(" P50:", metrics.latency.p50.toFixed(2))); console.log(formatRow(" P95:", metrics.latency.p95.toFixed(2))); console.log(formatRow(" P99:", metrics.latency.p99.toFixed(2))); } // Tracking duration if (metrics.trackingDurationMs) { const durationSec = metrics.trackingDurationMs / 1000; const throughput = metrics.totalSpans > 0 ? metrics.totalSpans / durationSec : 0; console.log(""); console.log(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`)); } console.log(""); } } catch (error) { if (spinner) spinner.fail("Failed to get statistics"); logger.error("Error:", error instanceof Error ? error.message : String(error)); process.exit(1); } }, }; } } /** * Validate exporter configuration */ function validateExporterConfig(exporter, config) { const requiredFieldsMap = { langfuse: ["publicKey", "secretKey"], langsmith: ["apiKey"], otel: ["endpoint"], datadog: ["apiKey"], sentry: ["dsn"], braintrust: ["apiKey", "projectName"], arize: ["spaceKey", "apiKey"], posthog: ["apiKey"], laminar: ["apiKey"], }; const requiredFields = requiredFieldsMap[exporter]; const missingFields = requiredFields.filter((field) => !(field in config) || !config[field]); if (missingFields.length > 0) { return { valid: false, error: `Missing required fields: ${missingFields.join(", ")}`, requiredFields, }; } return { valid: true }; } /** * Get environment variable names for an exporter */ function getExporterEnvVars(exporter) { const envVarsMap = { langfuse: { LANGFUSE_PUBLIC_KEY: "your-public-key", LANGFUSE_SECRET_KEY: "your-secret-key", LANGFUSE_BASEURL: "https://cloud.langfuse.com (optional)", }, langsmith: { LANGCHAIN_API_KEY: "your-api-key", LANGCHAIN_PROJECT: "your-project-name (optional)", LANGCHAIN_ENDPOINT: "https://api.smith.langchain.com (optional)", }, otel: { OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318", OTEL_SERVICE_NAME: "your-service-name", OTEL_EXPORTER_OTLP_PROTOCOL: "http (or grpc)", }, datadog: { DD_API_KEY: "your-api-key", DD_SITE: "datadoghq.com (or regional site)", DD_SERVICE: "your-service-name", }, sentry: { SENTRY_DSN: "your-dsn-url", SENTRY_TRACES_SAMPLE_RATE: "1.0 (optional)", SENTRY_RELEASE: "your-release-version (optional)", }, braintrust: { BRAINTRUST_API_KEY: "your-api-key", BRAINTRUST_PROJECT: "your-project-name", }, arize: { ARIZE_SPACE_KEY: "your-space-key", ARIZE_API_KEY: "your-api-key", }, posthog: { POSTHOG_API_KEY: "your-api-key", POSTHOG_HOST: "https://app.posthog.com (optional)", }, laminar: { LAMINAR_API_KEY: "your-api-key", LAMINAR_BASE_URL: "https://api.laminar.run (optional)", }, }; return envVarsMap[exporter]; } /** * Get description for an exporter */ function getExporterDescription(exporter) { const descriptions = { langfuse: "Open-source LLM observability platform with traces and analytics", langsmith: "LangChain's platform for LLM application debugging and monitoring", otel: "OpenTelemetry Protocol (OTLP) for distributed tracing", datadog: "APM and infrastructure monitoring platform", sentry: "Error tracking and performance monitoring", braintrust: "AI evaluation and experimentation platform", arize: "ML observability platform for model monitoring", posthog: "Product analytics with LLM event tracking", laminar: "LLM application monitoring and debugging", }; return descriptions[exporter]; } //# sourceMappingURL=telemetry.js.map