@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
304 lines (303 loc) • 10.8 kB
JavaScript
/**
* Laminar Exporter
* Exports spans to Laminar LLM tracing platform
* @see https://docs.laminar.run/
*/
import { logger } from "../../utils/logger.js";
import { SpanStatus, SpanType } from "../../types/index.js";
import { BaseExporter } from "./baseExporter.js";
/**
* Laminar exporter for LLM pipeline tracing and monitoring
* Supports detailed traces with input/output tracking
*/
export class LaminarExporter extends BaseExporter {
apiKey;
projectApiKey;
baseUrl;
constructor(config) {
super("laminar", config);
this.apiKey = config.apiKey;
this.projectApiKey = config.projectApiKey;
this.baseUrl = config.baseUrl ?? "https://api.laminar.run";
}
async initialize() {
if (this.initialized) {
return;
}
// Verify API key by making a test call
try {
const response = await fetch(`${this.baseUrl}/v1/health`, {
headers: this.getHeaders(),
});
if (!response.ok) {
logger.warn("[Laminar] Could not verify API connection:", response.statusText);
}
}
catch (error) {
logger.warn("[Laminar] Could not verify API connection:", error instanceof Error ? error.message : error);
}
this.initialized = true;
this.startFlushInterval(this.config.flushIntervalMs ?? 5000);
}
/**
* Get authorization headers
*/
getHeaders() {
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
};
// Add project API key if provided
if (this.projectApiKey) {
headers["X-Project-Api-Key"] = this.projectApiKey;
}
return headers;
}
async exportSpan(span) {
const startTime = Date.now();
try {
const trace = this.convertToLaminarTrace(span);
const response = await fetch(`${this.baseUrl}/v1/traces`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify(trace),
});
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`);
}
return this.createSuccessResult(1, Date.now() - startTime);
}
catch (error) {
return this.createFailureResult([span.spanId], error instanceof Error ? error.message : String(error), Date.now() - startTime);
}
}
async exportBatch(spans) {
const startTime = Date.now();
try {
const traces = spans.map((s) => this.convertToLaminarTrace(s));
const response = await fetch(`${this.baseUrl}/v1/traces/batch`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify({ traces }),
});
if (!response.ok) {
throw new Error(`Batch export failed: ${response.statusText}`);
}
return this.createSuccessResult(spans.length, Date.now() - startTime);
}
catch (error) {
return this.createFailureResult(spans.map((s) => s.spanId), error instanceof Error ? error.message : String(error), Date.now() - startTime);
}
}
async flush() {
if (this.buffer.length > 0) {
const spans = [...this.buffer];
this.buffer = [];
await this.exportBatch(spans);
}
}
async shutdown() {
await this.flush();
this.stopFlushInterval();
this.initialized = false;
}
async healthCheck() {
try {
await this.withRetry(() => this.ping(), "health check");
return this.createHealthStatus(true);
}
catch {
return this.createHealthStatus(false, ["Health check failed"]);
}
}
/**
* Verify connectivity to Laminar API
*/
async ping() {
const response = await fetch(`${this.baseUrl}/v1/health`, {
headers: this.getHeaders(),
});
if (!response.ok) {
throw new Error(`Laminar API unreachable: ${response.status}`);
}
}
/**
* Convert span to Laminar trace format
*/
convertToLaminarTrace(span) {
return {
// Core identifiers
trace_id: span.traceId,
span_id: span.spanId,
parent_span_id: span.parentSpanId,
// Trace metadata
name: span.name,
type: this.mapSpanTypeToLaminarType(span.type),
start_time: span.startTime,
end_time: span.endTime,
duration_ms: span.durationMs,
// Status
status: this.mapSpanStatus(span.status),
status_message: span.statusMessage,
// Model information
model: {
provider: span.attributes["ai.provider"],
name: span.attributes["ai.model"],
version: span.attributes["ai.model.version"],
},
// Token usage
usage: {
input_tokens: span.attributes["ai.tokens.input"],
output_tokens: span.attributes["ai.tokens.output"],
total_tokens: span.attributes["ai.tokens.total"],
cache_read_tokens: span.attributes["ai.tokens.cache_read"],
cache_creation_tokens: span.attributes["ai.tokens.cache_creation"],
reasoning_tokens: span.attributes["ai.tokens.reasoning"],
},
// Cost tracking
cost: {
input: span.attributes["ai.cost.input"],
output: span.attributes["ai.cost.output"],
total: span.attributes["ai.cost.total"],
currency: span.attributes["ai.cost.currency"] || "USD",
},
// Generation parameters
parameters: {
temperature: span.attributes["ai.temperature"],
max_tokens: span.attributes["ai.max_tokens"],
top_p: span.attributes["ai.top_p"],
stop_sequences: span.attributes["ai.stop_sequences"],
},
// Input/Output
input: span.attributes["input"],
output: span.attributes["output"],
// Tool information
tool: span.attributes["tool.name"]
? {
name: span.attributes["tool.name"],
server: span.attributes["tool.server"],
success: span.attributes["tool.success"],
}
: undefined,
// Error information
error: span.status === SpanStatus.ERROR
? {
type: span.attributes["error.type"],
message: span.attributes["error.message"],
stack: span.attributes["error.stack"],
}
: undefined,
// User and session context
context: {
user_id: span.attributes["user.id"],
session_id: span.attributes["session.id"],
environment: span.attributes["deployment.environment"] || this.config.environment,
service: {
name: span.attributes["service.name"],
version: span.attributes["service.version"] || this.config.version,
},
},
// Events
events: span.events.map((event) => ({
name: event.name,
timestamp: event.timestamp,
attributes: event.attributes,
})),
// Links to related spans
links: span.links.map((link) => ({
trace_id: link.traceId,
span_id: link.spanId,
attributes: link.attributes,
})),
// Additional metadata
metadata: this.extractMetadata(span.attributes),
};
}
/**
* Map NeuroLink span type to Laminar type
*/
mapSpanTypeToLaminarType(type) {
const typeMap = {
[SpanType.AGENT_RUN]: "agent",
[SpanType.WORKFLOW_STEP]: "workflow",
[SpanType.TOOL_CALL]: "tool",
[SpanType.MODEL_GENERATION]: "llm",
[SpanType.EMBEDDING]: "embedding",
[SpanType.RETRIEVAL]: "retrieval",
[SpanType.MEMORY]: "memory",
[SpanType.CONTEXT_COMPACTION]: "custom",
[SpanType.RAG]: "retrieval",
[SpanType.EVALUATION]: "custom",
[SpanType.MCP_TRANSPORT]: "tool",
[SpanType.MEDIA_GENERATION]: "llm",
[SpanType.PPT_GENERATION]: "custom",
[SpanType.WORKFLOW]: "workflow",
[SpanType.TTS]: "custom",
[SpanType.STT]: "custom",
[SpanType.SERVER_REQUEST]: "custom",
[SpanType.CUSTOM]: "custom",
};
return typeMap[type] || "custom";
}
/**
* Map span status to Laminar status format
*/
mapSpanStatus(status) {
switch (status) {
case SpanStatus.OK:
return "success";
case SpanStatus.ERROR:
return "error";
default:
return "unset";
}
}
/**
* Extract additional metadata from span attributes
* Filters out standard attributes that are already handled
*/
extractMetadata(attributes) {
const standardKeys = new Set([
"service.name",
"service.version",
"deployment.environment",
"user.id",
"session.id",
"ai.provider",
"ai.model",
"ai.model.version",
"ai.tokens.input",
"ai.tokens.output",
"ai.tokens.total",
"ai.tokens.cache_read",
"ai.tokens.cache_creation",
"ai.tokens.reasoning",
"ai.cost.input",
"ai.cost.output",
"ai.cost.total",
"ai.cost.currency",
"ai.temperature",
"ai.max_tokens",
"ai.top_p",
"ai.stop_sequences",
"tool.name",
"tool.server",
"tool.success",
"error.type",
"error.message",
"error.stack",
"error",
"input",
"output",
"expected",
"scores",
]);
const metadata = {};
for (const [key, value] of Object.entries(attributes)) {
if (!standardKeys.has(key) && value !== undefined) {
metadata[key] = value;
}
}
return metadata;
}
}