@juspay/neurolink 10.0.0 → 10.0.1
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 +6 -0
- package/dist/cli/commands/observability.js +114 -103
- package/dist/cli/commands/telemetry.js +139 -119
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [10.0.1](https://github.com/juspay/neurolink/compare/v10.0.0...v10.0.1) (2026-07-20)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- **(cli):** redact URL credentials in observability/telemetry output and clear flush timer leak ([cbb91a4](https://github.com/juspay/neurolink/commit/cbb91a4957520e798a13b09f2486e63b27ed3590))
|
|
6
|
+
|
|
1
7
|
## [10.0.0](https://github.com/juspay/neurolink/compare/v9.95.3...v10.0.0) (2026-07-20)
|
|
2
8
|
|
|
3
9
|
### ⚠ BREAKING CHANGES
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console, curly */
|
|
3
2
|
/**
|
|
4
3
|
* NeuroLink CLI Observability Commands
|
|
5
4
|
*
|
|
@@ -14,6 +13,7 @@ import ora from "ora";
|
|
|
14
13
|
import { logger } from "../../lib/utils/logger.js";
|
|
15
14
|
import { NeuroLink } from "../../lib/neurolink.js";
|
|
16
15
|
import { formatRow, formatCost } from "../utils/formatters.js";
|
|
16
|
+
import { redactUrlCredentials } from "../../lib/utils/logSanitize.js";
|
|
17
17
|
/**
|
|
18
18
|
* Observability Command Factory
|
|
19
19
|
*/
|
|
@@ -70,67 +70,72 @@ export class ObservabilityCommandFactory {
|
|
|
70
70
|
try {
|
|
71
71
|
const neurolink = new NeuroLink();
|
|
72
72
|
const status = neurolink.getTelemetryStatus();
|
|
73
|
-
if (spinner)
|
|
73
|
+
if (spinner) {
|
|
74
74
|
spinner.succeed("Status retrieved");
|
|
75
|
+
}
|
|
75
76
|
if (args.format === "json") {
|
|
76
|
-
|
|
77
|
+
logger.always(JSON.stringify(status, (_key, value) => typeof value === "string" &&
|
|
78
|
+
(_key === "baseUrl" || _key === "endpoint")
|
|
79
|
+
? redactUrlCredentials(value)
|
|
80
|
+
: value, 2));
|
|
77
81
|
}
|
|
78
82
|
else {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
logger.always("");
|
|
84
|
+
logger.always(chalk.bold.cyan("=== Observability Status ==="));
|
|
85
|
+
logger.always("");
|
|
82
86
|
// Telemetry enabled status
|
|
83
87
|
const enabledIcon = status.enabled
|
|
84
88
|
? chalk.green("ON")
|
|
85
89
|
: chalk.red("OFF");
|
|
86
|
-
|
|
90
|
+
logger.always(formatRow("Telemetry:", enabledIcon));
|
|
87
91
|
// Langfuse status
|
|
88
92
|
if (status.langfuse) {
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
logger.always("");
|
|
94
|
+
logger.always(chalk.bold("Langfuse:"));
|
|
91
95
|
const lfStatus = status.langfuse.enabled
|
|
92
96
|
? chalk.green("Enabled")
|
|
93
97
|
: chalk.gray("Disabled");
|
|
94
|
-
|
|
98
|
+
logger.always(formatRow(" Status:", lfStatus));
|
|
95
99
|
if (status.langfuse.baseUrl) {
|
|
96
|
-
|
|
100
|
+
logger.always(formatRow(" URL:", redactUrlCredentials(status.langfuse.baseUrl)));
|
|
97
101
|
}
|
|
98
102
|
if (status.langfuse.environment) {
|
|
99
|
-
|
|
103
|
+
logger.always(formatRow(" Environment:", status.langfuse.environment));
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
// OpenTelemetry status
|
|
103
107
|
if (status.openTelemetry) {
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
logger.always("");
|
|
109
|
+
logger.always(chalk.bold("OpenTelemetry:"));
|
|
106
110
|
const otelStatus = status.openTelemetry.enabled
|
|
107
111
|
? chalk.green("Enabled")
|
|
108
112
|
: chalk.gray("Disabled");
|
|
109
|
-
|
|
113
|
+
logger.always(formatRow(" Status:", otelStatus));
|
|
110
114
|
if (status.openTelemetry.endpoint) {
|
|
111
|
-
|
|
115
|
+
logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
|
|
112
116
|
}
|
|
113
117
|
if (status.openTelemetry.serviceName) {
|
|
114
|
-
|
|
118
|
+
logger.always(formatRow(" Service:", status.openTelemetry.serviceName));
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
// Exporters summary
|
|
118
122
|
if (status.exporters && status.exporters.length > 0) {
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
logger.always("");
|
|
124
|
+
logger.always(chalk.bold("Active Exporters:"));
|
|
121
125
|
for (const exporter of status.exporters) {
|
|
122
126
|
const exporterStatus = exporter.healthy
|
|
123
127
|
? chalk.green("Healthy")
|
|
124
128
|
: chalk.red("Unhealthy");
|
|
125
|
-
|
|
129
|
+
logger.always(formatRow(` ${exporter.name}:`, exporterStatus));
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
|
-
|
|
132
|
+
logger.always("");
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
catch (error) {
|
|
132
|
-
if (spinner)
|
|
136
|
+
if (spinner) {
|
|
133
137
|
spinner.fail("Failed to get status");
|
|
138
|
+
}
|
|
134
139
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
135
140
|
process.exit(1);
|
|
136
141
|
}
|
|
@@ -171,74 +176,76 @@ export class ObservabilityCommandFactory {
|
|
|
171
176
|
try {
|
|
172
177
|
const neurolink = new NeuroLink();
|
|
173
178
|
const metrics = neurolink.getMetrics();
|
|
174
|
-
if (spinner)
|
|
179
|
+
if (spinner) {
|
|
175
180
|
spinner.succeed("Metrics retrieved");
|
|
181
|
+
}
|
|
176
182
|
if (args.format === "json") {
|
|
177
|
-
|
|
183
|
+
logger.always(JSON.stringify(metrics, null, 2));
|
|
178
184
|
}
|
|
179
185
|
else {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
logger.always("");
|
|
187
|
+
logger.always(chalk.bold.cyan("=== Metrics Summary ==="));
|
|
188
|
+
logger.always("");
|
|
183
189
|
// Request statistics
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
logger.always(chalk.bold("Request Statistics:"));
|
|
191
|
+
logger.always(formatRow(" Total requests:", metrics.totalSpans.toLocaleString()));
|
|
192
|
+
logger.always(formatRow(" Successful:", metrics.successfulSpans.toLocaleString()));
|
|
193
|
+
logger.always(formatRow(" Failed:", metrics.failedSpans.toLocaleString()));
|
|
194
|
+
logger.always(formatRow(" Success rate:", `${(metrics.successRate * 100).toFixed(2)}%`));
|
|
189
195
|
// Latency statistics
|
|
190
196
|
if (metrics.latency && metrics.latency.count > 0) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
logger.always("");
|
|
198
|
+
logger.always(chalk.bold("Latency (ms):"));
|
|
199
|
+
logger.always(formatRow(" Min:", metrics.latency.min.toFixed(2)));
|
|
200
|
+
logger.always(formatRow(" Max:", metrics.latency.max.toFixed(2)));
|
|
201
|
+
logger.always(formatRow(" Mean:", metrics.latency.mean.toFixed(2)));
|
|
202
|
+
logger.always(formatRow(" P50:", metrics.latency.p50.toFixed(2)));
|
|
203
|
+
logger.always(formatRow(" P95:", metrics.latency.p95.toFixed(2)));
|
|
204
|
+
logger.always(formatRow(" P99:", metrics.latency.p99.toFixed(2)));
|
|
199
205
|
if (args.detailed) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
logger.always(formatRow(" P75:", metrics.latency.p75.toFixed(2)));
|
|
207
|
+
logger.always(formatRow(" P90:", metrics.latency.p90.toFixed(2)));
|
|
208
|
+
logger.always(formatRow(" Std Dev:", metrics.latency.stdDev.toFixed(2)));
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
211
|
// Token statistics
|
|
206
212
|
if (metrics.tokens) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
logger.always("");
|
|
214
|
+
logger.always(chalk.bold("Token Usage:"));
|
|
215
|
+
logger.always(formatRow(" Input tokens:", metrics.tokens.totalInputTokens.toLocaleString()));
|
|
216
|
+
logger.always(formatRow(" Output tokens:", metrics.tokens.totalOutputTokens.toLocaleString()));
|
|
217
|
+
logger.always(formatRow(" Total tokens:", metrics.tokens.totalTokens.toLocaleString()));
|
|
212
218
|
if (args.detailed &&
|
|
213
219
|
metrics.tokens.cacheReadTokens &&
|
|
214
220
|
metrics.tokens.cacheReadTokens > 0) {
|
|
215
|
-
|
|
221
|
+
logger.always(formatRow(" Cache read:", metrics.tokens.cacheReadTokens.toLocaleString()));
|
|
216
222
|
}
|
|
217
223
|
if (args.detailed &&
|
|
218
224
|
metrics.tokens.reasoningTokens &&
|
|
219
225
|
metrics.tokens.reasoningTokens > 0) {
|
|
220
|
-
|
|
226
|
+
logger.always(formatRow(" Reasoning:", metrics.tokens.reasoningTokens.toLocaleString()));
|
|
221
227
|
}
|
|
222
228
|
}
|
|
223
229
|
// Cost
|
|
224
230
|
if (metrics.totalCost !== undefined && metrics.totalCost > 0) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
logger.always("");
|
|
232
|
+
logger.always(chalk.bold("Cost:"));
|
|
233
|
+
logger.always(formatRow(" Total:", formatCost(metrics.totalCost)));
|
|
228
234
|
}
|
|
229
235
|
// Tracking duration
|
|
230
236
|
if (metrics.trackingDurationMs) {
|
|
231
237
|
const durationSec = metrics.trackingDurationMs / 1000;
|
|
232
238
|
const throughput = metrics.totalSpans > 0 ? metrics.totalSpans / durationSec : 0;
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
logger.always("");
|
|
240
|
+
logger.always(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`));
|
|
235
241
|
}
|
|
236
|
-
|
|
242
|
+
logger.always("");
|
|
237
243
|
}
|
|
238
244
|
}
|
|
239
245
|
catch (error) {
|
|
240
|
-
if (spinner)
|
|
246
|
+
if (spinner) {
|
|
241
247
|
spinner.fail("Failed to get metrics");
|
|
248
|
+
}
|
|
242
249
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
243
250
|
process.exit(1);
|
|
244
251
|
}
|
|
@@ -276,60 +283,62 @@ export class ObservabilityCommandFactory {
|
|
|
276
283
|
try {
|
|
277
284
|
const neurolink = new NeuroLink();
|
|
278
285
|
const status = neurolink.getTelemetryStatus();
|
|
279
|
-
if (spinner)
|
|
286
|
+
if (spinner) {
|
|
280
287
|
spinner.succeed("Exporters retrieved");
|
|
288
|
+
}
|
|
281
289
|
const exporters = status.exporters ?? [];
|
|
282
290
|
if (args.format === "json") {
|
|
283
|
-
|
|
291
|
+
logger.always(JSON.stringify(exporters, null, 2));
|
|
284
292
|
}
|
|
285
293
|
else {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
294
|
+
logger.always("");
|
|
295
|
+
logger.always(chalk.bold.cyan("=== Configured Exporters ==="));
|
|
296
|
+
logger.always("");
|
|
289
297
|
if (exporters.length === 0) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
logger.always(chalk.gray("No exporters configured."));
|
|
299
|
+
logger.always("");
|
|
300
|
+
logger.always("Available exporters:");
|
|
301
|
+
logger.always(" - Langfuse (langfuse)");
|
|
302
|
+
logger.always(" - LangSmith (langsmith)");
|
|
303
|
+
logger.always(" - OpenTelemetry (otel)");
|
|
304
|
+
logger.always(" - Datadog (datadog)");
|
|
305
|
+
logger.always(" - Sentry (sentry)");
|
|
306
|
+
logger.always(" - Braintrust (braintrust)");
|
|
307
|
+
logger.always(" - Arize (arize)");
|
|
308
|
+
logger.always(" - PostHog (posthog)");
|
|
309
|
+
logger.always(" - Laminar (laminar)");
|
|
302
310
|
}
|
|
303
311
|
else {
|
|
304
312
|
for (const exporter of exporters) {
|
|
305
313
|
const healthIcon = exporter.healthy
|
|
306
314
|
? chalk.green("[OK]")
|
|
307
315
|
: chalk.red("[ERROR]");
|
|
308
|
-
|
|
316
|
+
logger.always(`${healthIcon} ${chalk.bold(exporter.name)}`);
|
|
309
317
|
if (exporter.latencyMs !== undefined) {
|
|
310
|
-
|
|
318
|
+
logger.always(formatRow(" Latency:", `${exporter.latencyMs.toFixed(2)}ms`));
|
|
311
319
|
}
|
|
312
320
|
if (exporter.pendingSpans !== undefined) {
|
|
313
|
-
|
|
321
|
+
logger.always(formatRow(" Pending spans:", exporter.pendingSpans.toLocaleString()));
|
|
314
322
|
}
|
|
315
323
|
if (exporter.lastExportTime) {
|
|
316
324
|
const lastExport = new Date(exporter.lastExportTime);
|
|
317
|
-
|
|
325
|
+
logger.always(formatRow(" Last export:", lastExport.toISOString()));
|
|
318
326
|
}
|
|
319
327
|
if (exporter.errors && exporter.errors.length > 0) {
|
|
320
|
-
|
|
328
|
+
logger.always(chalk.yellow(" Errors:"));
|
|
321
329
|
for (const error of exporter.errors.slice(0, 3)) {
|
|
322
|
-
|
|
330
|
+
logger.always(chalk.red(` - ${error}`));
|
|
323
331
|
}
|
|
324
332
|
}
|
|
325
|
-
|
|
333
|
+
logger.always("");
|
|
326
334
|
}
|
|
327
335
|
}
|
|
328
336
|
}
|
|
329
337
|
}
|
|
330
338
|
catch (error) {
|
|
331
|
-
if (spinner)
|
|
339
|
+
if (spinner) {
|
|
332
340
|
spinner.fail("Failed to get exporters");
|
|
341
|
+
}
|
|
333
342
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
334
343
|
process.exit(1);
|
|
335
344
|
}
|
|
@@ -377,8 +386,9 @@ export class ObservabilityCommandFactory {
|
|
|
377
386
|
try {
|
|
378
387
|
const neurolink = new NeuroLink();
|
|
379
388
|
const metrics = neurolink.getMetrics();
|
|
380
|
-
if (spinner)
|
|
389
|
+
if (spinner) {
|
|
381
390
|
spinner.succeed("Costs calculated");
|
|
391
|
+
}
|
|
382
392
|
if (args.format === "json") {
|
|
383
393
|
const jsonOutput = {
|
|
384
394
|
totalCost: metrics.totalCost,
|
|
@@ -389,60 +399,61 @@ export class ObservabilityCommandFactory {
|
|
|
389
399
|
if (args.byModel !== false) {
|
|
390
400
|
jsonOutput.costByModel = metrics.costByModel;
|
|
391
401
|
}
|
|
392
|
-
|
|
402
|
+
logger.always(JSON.stringify(jsonOutput, null, 2));
|
|
393
403
|
}
|
|
394
404
|
else {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
405
|
+
logger.always("");
|
|
406
|
+
logger.always(chalk.bold.cyan("=== Cost Breakdown ==="));
|
|
407
|
+
logger.always("");
|
|
398
408
|
// Total cost
|
|
399
|
-
|
|
409
|
+
logger.always(chalk.bold(`Total Cost: ${formatCost(metrics.totalCost ?? 0)}`));
|
|
400
410
|
// Cost by provider
|
|
401
411
|
if (args.byProvider !== false &&
|
|
402
412
|
metrics.costByProvider &&
|
|
403
413
|
metrics.costByProvider.length > 0) {
|
|
404
|
-
|
|
405
|
-
|
|
414
|
+
logger.always("");
|
|
415
|
+
logger.always(chalk.bold("By Provider:"));
|
|
406
416
|
// Sort by cost descending
|
|
407
417
|
const sortedProviders = [...metrics.costByProvider].sort((a, b) => b.totalCost - a.totalCost);
|
|
408
418
|
for (const provider of sortedProviders) {
|
|
409
419
|
const costStr = formatCost(provider.totalCost);
|
|
410
420
|
const avgCost = formatCost(provider.avgCostPerRequest);
|
|
411
|
-
|
|
412
|
-
|
|
421
|
+
logger.always(` ${chalk.cyan(provider.provider.padEnd(15))} ${costStr}`);
|
|
422
|
+
logger.always(chalk.gray(` ${provider.requestCount} requests, avg ${avgCost}/req`));
|
|
413
423
|
}
|
|
414
424
|
}
|
|
415
425
|
// Cost by model
|
|
416
426
|
if (args.byModel !== false &&
|
|
417
427
|
metrics.costByModel &&
|
|
418
428
|
metrics.costByModel.length > 0) {
|
|
419
|
-
|
|
420
|
-
|
|
429
|
+
logger.always("");
|
|
430
|
+
logger.always(chalk.bold("By Model:"));
|
|
421
431
|
// Sort by cost descending
|
|
422
432
|
const sortedModels = [...metrics.costByModel].sort((a, b) => b.totalCost - a.totalCost);
|
|
423
433
|
for (const model of sortedModels) {
|
|
424
434
|
const costStr = formatCost(model.totalCost);
|
|
425
435
|
const avgCost = formatCost(model.avgCostPerRequest);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
436
|
+
logger.always(` ${chalk.cyan(model.model)}`);
|
|
437
|
+
logger.always(` Cost: ${costStr}`);
|
|
438
|
+
logger.always(chalk.gray(` ${model.requestCount} requests, avg ${avgCost}/req`));
|
|
439
|
+
logger.always(chalk.gray(` ${model.inputTokens.toLocaleString()} input, ${model.outputTokens.toLocaleString()} output tokens`));
|
|
430
440
|
}
|
|
431
441
|
}
|
|
432
442
|
// No cost data
|
|
433
443
|
if ((!metrics.costByProvider ||
|
|
434
444
|
metrics.costByProvider.length === 0) &&
|
|
435
445
|
(!metrics.costByModel || metrics.costByModel.length === 0)) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
446
|
+
logger.always("");
|
|
447
|
+
logger.always(chalk.gray("No cost data available."));
|
|
448
|
+
logger.always(chalk.gray("Cost tracking is recorded when observability is enabled."));
|
|
439
449
|
}
|
|
440
|
-
|
|
450
|
+
logger.always("");
|
|
441
451
|
}
|
|
442
452
|
}
|
|
443
453
|
catch (error) {
|
|
444
|
-
if (spinner)
|
|
454
|
+
if (spinner) {
|
|
445
455
|
spinner.fail("Failed to get costs");
|
|
456
|
+
}
|
|
446
457
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
447
458
|
process.exit(1);
|
|
448
459
|
}
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.1",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|