@juspay/neurolink 10.0.0 → 10.1.0
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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +305 -305
- package/dist/cli/commands/observability.js +114 -103
- package/dist/cli/commands/proxyReplay.d.ts +3 -0
- package/dist/cli/commands/proxyReplay.js +175 -0
- package/dist/cli/commands/telemetry.js +139 -119
- package/dist/cli/parser.js +3 -1
- package/dist/lib/proxy/proxyReplay.d.ts +10 -0
- package/dist/lib/proxy/proxyReplay.js +765 -0
- package/dist/lib/proxy/requestLogger.d.ts +13 -0
- package/dist/lib/proxy/requestLogger.js +13 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +9 -2
- package/dist/lib/types/cli.d.ts +16 -0
- package/dist/lib/types/proxy.d.ts +121 -0
- package/dist/proxy/proxyReplay.d.ts +10 -0
- package/dist/proxy/proxyReplay.js +764 -0
- package/dist/proxy/requestLogger.d.ts +13 -0
- package/dist/proxy/requestLogger.js +13 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +9 -2
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/proxy.d.ts +121 -0
- package/package.json +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console, curly */
|
|
3
2
|
/**
|
|
4
3
|
* NeuroLink CLI Telemetry Commands
|
|
5
4
|
*
|
|
@@ -16,6 +15,7 @@ import { logger } from "../../lib/utils/logger.js";
|
|
|
16
15
|
import { NeuroLink } from "../../lib/neurolink.js";
|
|
17
16
|
import { flushOpenTelemetry } from "../../lib/services/server/ai/observability/instrumentation.js";
|
|
18
17
|
import { formatRow, formatCost } from "../utils/formatters.js";
|
|
18
|
+
import { redactUrlCredentials } from "../../lib/utils/logSanitize.js";
|
|
19
19
|
/**
|
|
20
20
|
* Available exporter names
|
|
21
21
|
*/
|
|
@@ -87,54 +87,58 @@ export class TelemetryCommandFactory {
|
|
|
87
87
|
try {
|
|
88
88
|
const neurolink = new NeuroLink();
|
|
89
89
|
const status = neurolink.getTelemetryStatus();
|
|
90
|
-
if (spinner)
|
|
90
|
+
if (spinner) {
|
|
91
91
|
spinner.succeed("Status retrieved");
|
|
92
|
+
}
|
|
92
93
|
if (args.format === "json") {
|
|
93
|
-
|
|
94
|
+
logger.always(JSON.stringify(status, (_key, value) => typeof value === "string" &&
|
|
95
|
+
(_key === "baseUrl" || _key === "endpoint")
|
|
96
|
+
? redactUrlCredentials(value)
|
|
97
|
+
: value, 2));
|
|
94
98
|
}
|
|
95
99
|
else {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
logger.always("");
|
|
101
|
+
logger.always(chalk.bold.cyan("=== Telemetry Status ==="));
|
|
102
|
+
logger.always("");
|
|
99
103
|
// Telemetry enabled status
|
|
100
104
|
const enabledIcon = status.enabled
|
|
101
105
|
? chalk.green("ENABLED")
|
|
102
106
|
: chalk.red("DISABLED");
|
|
103
|
-
|
|
107
|
+
logger.always(formatRow("Telemetry:", enabledIcon));
|
|
104
108
|
// OpenTelemetry status
|
|
105
109
|
if (status.openTelemetry) {
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
logger.always("");
|
|
111
|
+
logger.always(chalk.bold("OpenTelemetry:"));
|
|
108
112
|
const otelStatus = status.openTelemetry.enabled
|
|
109
113
|
? chalk.green("Active")
|
|
110
114
|
: chalk.gray("Inactive");
|
|
111
|
-
|
|
115
|
+
logger.always(formatRow(" Status:", otelStatus));
|
|
112
116
|
if (status.openTelemetry.endpoint) {
|
|
113
|
-
|
|
117
|
+
logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
|
|
114
118
|
}
|
|
115
119
|
if (status.openTelemetry.serviceName) {
|
|
116
|
-
|
|
120
|
+
logger.always(formatRow(" Service:", status.openTelemetry.serviceName));
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
// Langfuse status
|
|
120
124
|
if (status.langfuse) {
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
logger.always("");
|
|
126
|
+
logger.always(chalk.bold("Langfuse:"));
|
|
123
127
|
const lfStatus = status.langfuse.enabled
|
|
124
128
|
? chalk.green("Active")
|
|
125
129
|
: chalk.gray("Inactive");
|
|
126
|
-
|
|
130
|
+
logger.always(formatRow(" Status:", lfStatus));
|
|
127
131
|
if (status.langfuse.baseUrl) {
|
|
128
|
-
|
|
132
|
+
logger.always(formatRow(" URL:", redactUrlCredentials(status.langfuse.baseUrl)));
|
|
129
133
|
}
|
|
130
134
|
if (status.langfuse.environment) {
|
|
131
|
-
|
|
135
|
+
logger.always(formatRow(" Environment:", status.langfuse.environment));
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
// Exporters health summary
|
|
135
139
|
if (status.exporters && status.exporters.length > 0) {
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
logger.always("");
|
|
141
|
+
logger.always(chalk.bold("Exporter Health:"));
|
|
138
142
|
for (const exporter of status.exporters) {
|
|
139
143
|
const healthIcon = exporter.healthy
|
|
140
144
|
? chalk.green("[OK]")
|
|
@@ -142,24 +146,25 @@ export class TelemetryCommandFactory {
|
|
|
142
146
|
const pendingInfo = exporter.pendingSpans
|
|
143
147
|
? chalk.gray(` (${exporter.pendingSpans} pending)`)
|
|
144
148
|
: "";
|
|
145
|
-
|
|
149
|
+
logger.always(` ${healthIcon} ${exporter.name}${pendingInfo}`);
|
|
146
150
|
if (exporter.errors && exporter.errors.length > 0) {
|
|
147
151
|
for (const error of exporter.errors.slice(0, 2)) {
|
|
148
|
-
|
|
152
|
+
logger.always(chalk.red(` Error: ${error}`));
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
else {
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
logger.always("");
|
|
159
|
+
logger.always(chalk.gray("No exporters configured."));
|
|
156
160
|
}
|
|
157
|
-
|
|
161
|
+
logger.always("");
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
catch (error) {
|
|
161
|
-
if (spinner)
|
|
165
|
+
if (spinner) {
|
|
162
166
|
spinner.fail("Failed to get status");
|
|
167
|
+
}
|
|
163
168
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
164
169
|
process.exit(1);
|
|
165
170
|
}
|
|
@@ -213,33 +218,36 @@ export class TelemetryCommandFactory {
|
|
|
213
218
|
config = JSON.parse(args.config);
|
|
214
219
|
}
|
|
215
220
|
catch {
|
|
216
|
-
if (spinner)
|
|
221
|
+
if (spinner) {
|
|
217
222
|
spinner.fail("Invalid JSON configuration");
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
}
|
|
224
|
+
logger.always(chalk.red("Error: Configuration must be valid JSON"));
|
|
225
|
+
logger.always("");
|
|
226
|
+
logger.always("Example:");
|
|
227
|
+
logger.always(chalk.gray(` neurolink telemetry configure --exporter langfuse --config '{"publicKey":"pk-...", "secretKey":"sk-..."}'`));
|
|
222
228
|
process.exit(1);
|
|
223
229
|
}
|
|
224
230
|
// Validate required fields based on exporter type
|
|
225
231
|
const validationResult = validateExporterConfig(args.exporter, config);
|
|
226
232
|
if (!validationResult.valid) {
|
|
227
|
-
if (spinner)
|
|
233
|
+
if (spinner) {
|
|
228
234
|
spinner.fail("Configuration validation failed");
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
}
|
|
236
|
+
logger.always(chalk.red(`Error: ${validationResult.error}`));
|
|
237
|
+
logger.always("");
|
|
238
|
+
logger.always(chalk.yellow(`Required fields for ${args.exporter}:`));
|
|
232
239
|
for (const field of validationResult.requiredFields ?? []) {
|
|
233
|
-
|
|
240
|
+
logger.always(chalk.gray(` - ${field}`));
|
|
234
241
|
}
|
|
235
242
|
process.exit(1);
|
|
236
243
|
}
|
|
237
244
|
// Currently, exporter configuration is done via environment variables
|
|
238
245
|
// or SDK initialization. This command provides guidance on how to configure.
|
|
239
|
-
if (spinner)
|
|
246
|
+
if (spinner) {
|
|
240
247
|
spinner.succeed(`${args.exporter} configuration validated`);
|
|
248
|
+
}
|
|
241
249
|
if (args.format === "json") {
|
|
242
|
-
|
|
250
|
+
logger.always(JSON.stringify({
|
|
243
251
|
exporter: args.exporter,
|
|
244
252
|
config: config,
|
|
245
253
|
valid: true,
|
|
@@ -247,32 +255,33 @@ export class TelemetryCommandFactory {
|
|
|
247
255
|
}, null, 2));
|
|
248
256
|
}
|
|
249
257
|
else {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
logger.always("");
|
|
259
|
+
logger.always(chalk.bold.cyan(`=== ${args.exporter} Configuration ===`));
|
|
260
|
+
logger.always("");
|
|
261
|
+
logger.always(chalk.green("Configuration validated successfully!"));
|
|
262
|
+
logger.always("");
|
|
263
|
+
logger.always(chalk.bold("To apply this configuration:"));
|
|
264
|
+
logger.always("");
|
|
257
265
|
// Show environment variable instructions
|
|
258
266
|
const envVars = getExporterEnvVars(args.exporter);
|
|
259
|
-
|
|
267
|
+
logger.always(chalk.yellow("Option 1: Set environment variables"));
|
|
260
268
|
for (const [key, description] of Object.entries(envVars)) {
|
|
261
|
-
|
|
269
|
+
logger.always(chalk.gray(` export ${key}="<${description}>"`));
|
|
262
270
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
+
logger.always("");
|
|
272
|
+
logger.always(chalk.yellow("Option 2: SDK initialization"));
|
|
273
|
+
logger.always(chalk.gray(` const neurolink = new NeuroLink({`));
|
|
274
|
+
logger.always(chalk.gray(` observability: {`));
|
|
275
|
+
logger.always(chalk.gray(` ${args.exporter}: ${JSON.stringify(config, null, 6).split("\n").join("\n ")}`));
|
|
276
|
+
logger.always(chalk.gray(` }`));
|
|
277
|
+
logger.always(chalk.gray(` });`));
|
|
278
|
+
logger.always("");
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
catch (error) {
|
|
274
|
-
if (spinner)
|
|
282
|
+
if (spinner) {
|
|
275
283
|
spinner.fail("Failed to configure exporter");
|
|
284
|
+
}
|
|
276
285
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
277
286
|
process.exit(1);
|
|
278
287
|
}
|
|
@@ -308,20 +317,21 @@ export class TelemetryCommandFactory {
|
|
|
308
317
|
try {
|
|
309
318
|
const neurolink = new NeuroLink();
|
|
310
319
|
const status = neurolink.getTelemetryStatus();
|
|
311
|
-
if (spinner)
|
|
320
|
+
if (spinner) {
|
|
312
321
|
spinner.succeed("Exporters listed");
|
|
322
|
+
}
|
|
313
323
|
const configuredExporters = status.exporters ?? [];
|
|
314
324
|
const configuredNames = new Set(configuredExporters.map((e) => e.name.toLowerCase()));
|
|
315
325
|
if (args.format === "json") {
|
|
316
|
-
|
|
326
|
+
logger.always(JSON.stringify({
|
|
317
327
|
available: AVAILABLE_EXPORTERS,
|
|
318
328
|
configured: configuredExporters,
|
|
319
329
|
}, null, 2));
|
|
320
330
|
}
|
|
321
331
|
else {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
332
|
+
logger.always("");
|
|
333
|
+
logger.always(chalk.bold.cyan("=== Available Exporters ==="));
|
|
334
|
+
logger.always("");
|
|
325
335
|
for (const exporter of AVAILABLE_EXPORTERS) {
|
|
326
336
|
const isConfigured = configuredNames.has(exporter);
|
|
327
337
|
const configuredExporter = configuredExporters.find((e) => e.name.toLowerCase() === exporter);
|
|
@@ -331,27 +341,28 @@ export class TelemetryCommandFactory {
|
|
|
331
341
|
: chalk.yellow("[CONFIGURED]")
|
|
332
342
|
: chalk.gray("[AVAILABLE]");
|
|
333
343
|
const description = getExporterDescription(exporter);
|
|
334
|
-
|
|
335
|
-
|
|
344
|
+
logger.always(`${statusIcon} ${chalk.bold(exporter)}`);
|
|
345
|
+
logger.always(chalk.gray(` ${description}`));
|
|
336
346
|
if (isConfigured && configuredExporter) {
|
|
337
347
|
if (configuredExporter.pendingSpans) {
|
|
338
|
-
|
|
348
|
+
logger.always(chalk.gray(` Pending spans: ${configuredExporter.pendingSpans}`));
|
|
339
349
|
}
|
|
340
350
|
if (configuredExporter.lastExportTime) {
|
|
341
351
|
const lastExport = new Date(configuredExporter.lastExportTime);
|
|
342
|
-
|
|
352
|
+
logger.always(chalk.gray(` Last export: ${lastExport.toISOString()}`));
|
|
343
353
|
}
|
|
344
354
|
}
|
|
345
|
-
|
|
355
|
+
logger.always("");
|
|
346
356
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
357
|
+
logger.always(chalk.bold("Configuration Help:"));
|
|
358
|
+
logger.always(chalk.gray(" Use 'neurolink telemetry configure --exporter <name> --config <json>' to configure an exporter"));
|
|
359
|
+
logger.always("");
|
|
350
360
|
}
|
|
351
361
|
}
|
|
352
362
|
catch (error) {
|
|
353
|
-
if (spinner)
|
|
363
|
+
if (spinner) {
|
|
354
364
|
spinner.fail("Failed to list exporters");
|
|
365
|
+
}
|
|
355
366
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
356
367
|
process.exit(1);
|
|
357
368
|
}
|
|
@@ -397,21 +408,27 @@ export class TelemetryCommandFactory {
|
|
|
397
408
|
// Count pending spans before flush
|
|
398
409
|
const pendingBefore = statusBefore.exporters?.reduce((sum, e) => sum + (e.pendingSpans ?? 0), 0) ?? 0;
|
|
399
410
|
// Create a timeout promise
|
|
411
|
+
let flushTimer;
|
|
400
412
|
const timeoutPromise = new Promise((_, reject) => {
|
|
401
|
-
setTimeout(() => reject(new Error("Flush operation timed out")), args.timeout ?? 30000);
|
|
413
|
+
flushTimer = setTimeout(() => reject(new Error("Flush operation timed out")), args.timeout ?? 30000);
|
|
402
414
|
});
|
|
403
415
|
// Flush OpenTelemetry spans
|
|
404
416
|
const flushPromise = flushOpenTelemetry();
|
|
405
417
|
// Race between flush and timeout
|
|
406
418
|
await Promise.race([flushPromise, timeoutPromise]);
|
|
419
|
+
// Cancel the timeout timer to avoid unhandled rejection
|
|
420
|
+
if (flushTimer !== undefined) {
|
|
421
|
+
clearTimeout(flushTimer);
|
|
422
|
+
}
|
|
407
423
|
// Get status after flush
|
|
408
424
|
const statusAfter = neurolink.getTelemetryStatus();
|
|
409
425
|
const pendingAfter = statusAfter.exporters?.reduce((sum, e) => sum + (e.pendingSpans ?? 0), 0) ?? 0;
|
|
410
426
|
const flushedCount = Math.max(0, pendingBefore - pendingAfter);
|
|
411
|
-
if (spinner)
|
|
427
|
+
if (spinner) {
|
|
412
428
|
spinner.succeed("Flush completed");
|
|
429
|
+
}
|
|
413
430
|
if (args.format === "json") {
|
|
414
|
-
|
|
431
|
+
logger.always(JSON.stringify({
|
|
415
432
|
success: true,
|
|
416
433
|
pendingBefore,
|
|
417
434
|
pendingAfter,
|
|
@@ -419,18 +436,19 @@ export class TelemetryCommandFactory {
|
|
|
419
436
|
}, null, 2));
|
|
420
437
|
}
|
|
421
438
|
else {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
439
|
+
logger.always("");
|
|
440
|
+
logger.always(chalk.bold.cyan("=== Flush Complete ==="));
|
|
441
|
+
logger.always("");
|
|
442
|
+
logger.always(formatRow("Spans before:", pendingBefore.toString()));
|
|
443
|
+
logger.always(formatRow("Spans after:", pendingAfter.toString()));
|
|
444
|
+
logger.always(formatRow("Flushed:", chalk.green(flushedCount.toString())));
|
|
445
|
+
logger.always("");
|
|
429
446
|
}
|
|
430
447
|
}
|
|
431
448
|
catch (error) {
|
|
432
|
-
if (spinner)
|
|
449
|
+
if (spinner) {
|
|
433
450
|
spinner.fail("Failed to flush spans");
|
|
451
|
+
}
|
|
434
452
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
435
453
|
process.exit(1);
|
|
436
454
|
}
|
|
@@ -485,10 +503,11 @@ export class TelemetryCommandFactory {
|
|
|
485
503
|
try {
|
|
486
504
|
const neurolink = new NeuroLink();
|
|
487
505
|
const metrics = neurolink.getMetrics();
|
|
488
|
-
if (spinner)
|
|
506
|
+
if (spinner) {
|
|
489
507
|
spinner.succeed("Statistics retrieved");
|
|
508
|
+
}
|
|
490
509
|
if (args.format === "json") {
|
|
491
|
-
|
|
510
|
+
logger.always(JSON.stringify({
|
|
492
511
|
tokens: {
|
|
493
512
|
input: metrics.tokens.totalInputTokens,
|
|
494
513
|
output: metrics.tokens.totalOutputTokens,
|
|
@@ -511,82 +530,83 @@ export class TelemetryCommandFactory {
|
|
|
511
530
|
}, null, 2));
|
|
512
531
|
}
|
|
513
532
|
else {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
533
|
+
logger.always("");
|
|
534
|
+
logger.always(chalk.bold.cyan("=== Token & Cost Statistics ==="));
|
|
535
|
+
logger.always("");
|
|
517
536
|
// Token usage
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
537
|
+
logger.always(chalk.bold("Token Usage:"));
|
|
538
|
+
logger.always(formatRow(" Input tokens:", metrics.tokens.totalInputTokens.toLocaleString()));
|
|
539
|
+
logger.always(formatRow(" Output tokens:", metrics.tokens.totalOutputTokens.toLocaleString()));
|
|
540
|
+
logger.always(formatRow(" Total tokens:", metrics.tokens.totalTokens.toLocaleString()));
|
|
522
541
|
if (args.detailed) {
|
|
523
542
|
if (metrics.tokens.cacheReadTokens > 0) {
|
|
524
|
-
|
|
543
|
+
logger.always(formatRow(" Cache read:", metrics.tokens.cacheReadTokens.toLocaleString()));
|
|
525
544
|
}
|
|
526
545
|
if (metrics.tokens.reasoningTokens > 0) {
|
|
527
|
-
|
|
546
|
+
logger.always(formatRow(" Reasoning:", metrics.tokens.reasoningTokens.toLocaleString()));
|
|
528
547
|
}
|
|
529
548
|
}
|
|
530
549
|
// Cost summary
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
550
|
+
logger.always("");
|
|
551
|
+
logger.always(chalk.bold("Cost Summary:"));
|
|
552
|
+
logger.always(formatRow(" Total cost:", formatCost(metrics.totalCost ?? 0)));
|
|
534
553
|
// Cost by provider
|
|
535
554
|
if (args.byProvider !== false &&
|
|
536
555
|
metrics.costByProvider &&
|
|
537
556
|
metrics.costByProvider.length > 0) {
|
|
538
|
-
|
|
539
|
-
|
|
557
|
+
logger.always("");
|
|
558
|
+
logger.always(chalk.bold("Cost by Provider:"));
|
|
540
559
|
const sortedProviders = [...metrics.costByProvider].sort((a, b) => b.totalCost - a.totalCost);
|
|
541
560
|
for (const provider of sortedProviders) {
|
|
542
|
-
|
|
543
|
-
|
|
561
|
+
logger.always(` ${chalk.cyan(provider.provider.padEnd(15))} ${formatCost(provider.totalCost)}`);
|
|
562
|
+
logger.always(chalk.gray(` ${provider.requestCount} requests, avg ${formatCost(provider.avgCostPerRequest)}/req`));
|
|
544
563
|
}
|
|
545
564
|
}
|
|
546
565
|
// Cost by model
|
|
547
566
|
if (args.byModel !== false &&
|
|
548
567
|
metrics.costByModel &&
|
|
549
568
|
metrics.costByModel.length > 0) {
|
|
550
|
-
|
|
551
|
-
|
|
569
|
+
logger.always("");
|
|
570
|
+
logger.always(chalk.bold("Cost by Model:"));
|
|
552
571
|
const sortedModels = [...metrics.costByModel].sort((a, b) => b.totalCost - a.totalCost);
|
|
553
572
|
for (const model of sortedModels) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
573
|
+
logger.always(` ${chalk.cyan(model.model)}`);
|
|
574
|
+
logger.always(` Cost: ${formatCost(model.totalCost)}`);
|
|
575
|
+
logger.always(chalk.gray(` ${model.requestCount} requests, avg ${formatCost(model.avgCostPerRequest)}/req`));
|
|
557
576
|
if (args.detailed) {
|
|
558
|
-
|
|
577
|
+
logger.always(chalk.gray(` ${model.inputTokens.toLocaleString()} input, ${model.outputTokens.toLocaleString()} output tokens`));
|
|
559
578
|
}
|
|
560
579
|
}
|
|
561
580
|
}
|
|
562
581
|
// Request statistics
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
582
|
+
logger.always("");
|
|
583
|
+
logger.always(chalk.bold("Request Statistics:"));
|
|
584
|
+
logger.always(formatRow(" Total requests:", metrics.totalSpans.toLocaleString()));
|
|
585
|
+
logger.always(formatRow(" Successful:", metrics.successfulSpans.toLocaleString()));
|
|
586
|
+
logger.always(formatRow(" Failed:", metrics.failedSpans.toLocaleString()));
|
|
587
|
+
logger.always(formatRow(" Success rate:", `${(metrics.successRate * 100).toFixed(2)}%`));
|
|
569
588
|
// Latency (if detailed)
|
|
570
589
|
if (args.detailed && metrics.latency.count > 0) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
590
|
+
logger.always("");
|
|
591
|
+
logger.always(chalk.bold("Latency (ms):"));
|
|
592
|
+
logger.always(formatRow(" P50:", metrics.latency.p50.toFixed(2)));
|
|
593
|
+
logger.always(formatRow(" P95:", metrics.latency.p95.toFixed(2)));
|
|
594
|
+
logger.always(formatRow(" P99:", metrics.latency.p99.toFixed(2)));
|
|
576
595
|
}
|
|
577
596
|
// Tracking duration
|
|
578
597
|
if (metrics.trackingDurationMs) {
|
|
579
598
|
const durationSec = metrics.trackingDurationMs / 1000;
|
|
580
599
|
const throughput = metrics.totalSpans > 0 ? metrics.totalSpans / durationSec : 0;
|
|
581
|
-
|
|
582
|
-
|
|
600
|
+
logger.always("");
|
|
601
|
+
logger.always(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`));
|
|
583
602
|
}
|
|
584
|
-
|
|
603
|
+
logger.always("");
|
|
585
604
|
}
|
|
586
605
|
}
|
|
587
606
|
catch (error) {
|
|
588
|
-
if (spinner)
|
|
607
|
+
if (spinner) {
|
|
589
608
|
spinner.fail("Failed to get statistics");
|
|
609
|
+
}
|
|
590
610
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
591
611
|
process.exit(1);
|
|
592
612
|
}
|
package/dist/cli/parser.js
CHANGED
|
@@ -15,6 +15,7 @@ import { ObservabilityCommandFactory } from "./commands/observability.js";
|
|
|
15
15
|
import { TelemetryCommandFactory } from "./commands/telemetry.js";
|
|
16
16
|
import { proxyStartCommand, proxyStatusCommand, proxyTelemetryCommand, proxySetupCommand, proxyGuardCommand, proxyInstallCommand, proxyUninstallCommand, } from "./commands/proxy.js";
|
|
17
17
|
import { proxyAnalyzeCommand } from "./commands/proxyAnalyze.js";
|
|
18
|
+
import { proxyReplayCommand } from "./commands/proxyReplay.js";
|
|
18
19
|
import { EvaluateCommandFactory } from "./commands/evaluate.js";
|
|
19
20
|
import { TaskCommandFactory } from "./commands/task.js";
|
|
20
21
|
import { AutoresearchCommandFactory } from "./commands/autoresearch.js";
|
|
@@ -202,12 +203,13 @@ export function initializeCliParser() {
|
|
|
202
203
|
.command(proxyStartCommand)
|
|
203
204
|
.command(proxyStatusCommand)
|
|
204
205
|
.command(proxyAnalyzeCommand)
|
|
206
|
+
.command(proxyReplayCommand)
|
|
205
207
|
.command(proxyTelemetryCommand)
|
|
206
208
|
.command(proxySetupCommand)
|
|
207
209
|
.command(proxyGuardCommand)
|
|
208
210
|
.command(proxyInstallCommand)
|
|
209
211
|
.command(proxyUninstallCommand)
|
|
210
|
-
.demandCommand(1, "Please specify a proxy subcommand: start, status, analyze, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
|
|
212
|
+
.demandCommand(1, "Please specify a proxy subcommand: start, status, analyze, replay <export|compare>, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
|
|
211
213
|
handler: () => { },
|
|
212
214
|
})
|
|
213
215
|
// Evaluate Command Group - Using EvaluateCommandFactory
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CompareProxyReplayOptions, ExportProxyReplayOptions, ProxyReplayBundle, ProxyReplayComparison } from "../types/index.js";
|
|
2
|
+
export declare function isValidProxyReplayHeaderName(name: string): boolean;
|
|
3
|
+
export declare function serializeProxyReplayDocument(value: unknown): string;
|
|
4
|
+
export declare function exportProxyReplayBundle(options: ExportProxyReplayOptions): Promise<ProxyReplayBundle>;
|
|
5
|
+
export declare function writeProxyReplayDocument(outputPath: string, document: unknown): Promise<{
|
|
6
|
+
path: string;
|
|
7
|
+
sha256: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function readProxyReplayBundle(bundlePath: string): Promise<ProxyReplayBundle>;
|
|
10
|
+
export declare function compareProxyReplayBundle(bundle: ProxyReplayBundle, options: CompareProxyReplayOptions): Promise<ProxyReplayComparison>;
|