@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
314 lines • 11.2 kB
JavaScript
/**
* Utility class for span creation and serialization
* Handles conversion between NeuroLink's span format and platform-specific formats
*/
import { randomBytes } from "node:crypto";
import { SpanStatus, SpanType, } from "../../types/index.js";
import { getActiveTraceContext } from "../../telemetry/traceContext.js";
/**
* Utility class for span creation and serialization
*/
export class SpanSerializer {
/**
* Create a new span with generated IDs.
*
* When `traceId` / `parentSpanId` are omitted, the method automatically
* attempts to inherit them from the active OTel context so that Pipeline B
* spans land inside the same Langfuse trace as Pipeline A spans (fix A5).
*/
static createSpan(type, name, attributes = {}, parentSpanId, traceId) {
// A5 fix: When no explicit traceId is provided, try to inherit from the
// active OTel context so Pipeline B spans are linked to Pipeline A traces.
let resolvedTraceId = traceId;
let resolvedParentSpanId = parentSpanId;
if (!resolvedTraceId) {
const otelCtx = getActiveTraceContext();
resolvedTraceId = otelCtx.traceId ?? randomBytes(16).toString("hex");
if (!resolvedParentSpanId && otelCtx.parentSpanId) {
resolvedParentSpanId = otelCtx.parentSpanId;
}
}
return {
spanId: randomBytes(8).toString("hex"),
traceId: resolvedTraceId,
parentSpanId: resolvedParentSpanId,
type,
name,
startTime: new Date().toISOString(),
status: SpanStatus.UNSET,
attributes: attributes,
events: [],
links: [],
};
}
/**
* End a span with status
*/
static endSpan(span, status = SpanStatus.OK, statusMessage) {
const endTime = new Date();
const startTime = new Date(span.startTime);
return {
...span,
endTime: endTime.toISOString(),
durationMs: endTime.getTime() - startTime.getTime(),
status,
statusMessage,
};
}
/**
* Add event to span
*/
static addEvent(span, name, attributes) {
const event = {
name,
timestamp: new Date().toISOString(),
attributes,
};
return {
...span,
events: [...span.events, event],
};
}
/**
* Update span attributes
*/
static updateAttributes(span, attributes) {
return {
...span,
attributes: {
...span.attributes,
...attributes,
},
};
}
/**
* Serialize span to JSON for export
*/
static toJSON(span) {
return JSON.stringify(span, null, 2);
}
/**
* Instance method to serialize a span object to JSON string
* @param span - The span data to serialize (can be partial span data)
* @returns JSON string representation of the span
*/
serialize(span) {
return JSON.stringify(span, null, 2);
}
/**
* Instance method to deserialize a JSON string to span data
* @param json - The JSON string to parse
* @returns Parsed span data
*/
deserialize(json) {
return JSON.parse(json);
}
/**
* Parse span from JSON
*/
static fromJSON(json) {
return JSON.parse(json);
}
/**
* Serialize span for Langfuse format
*/
static toLangfuseFormat(span) {
return {
id: span.spanId,
traceId: span.traceId,
parentObservationId: span.parentSpanId,
name: span.name,
startTime: span.startTime,
endTime: span.endTime,
// Only pick safe, non-PII attributes for metadata — intentionally excludes
// error.stack and other internal fields. input/output are exported here for
// Langfuse tracing; use LangfuseExporterConfig.redactIO=true to suppress them
// in compliance-sensitive deployments.
metadata: filterSafeMetadata(span.attributes),
level: span.status === SpanStatus.ERROR
? "ERROR"
: span.status === SpanStatus.WARNING
? "WARNING"
: "DEFAULT",
statusMessage: span.statusMessage,
input: span.attributes["input"],
output: span.attributes["output"],
usage: span.attributes["ai.tokens.total"] !== undefined
? {
promptTokens: span.attributes["ai.tokens.input"],
completionTokens: span.attributes["ai.tokens.output"],
totalTokens: span.attributes["ai.tokens.total"],
}
: undefined,
};
}
/**
* Serialize span for LangSmith format
*/
static toLangSmithFormat(span) {
return {
id: span.spanId,
trace_id: span.traceId,
parent_run_id: span.parentSpanId,
name: span.name,
run_type: SpanSerializer.mapSpanTypeToLangSmithRunType(span.type),
start_time: span.startTime,
end_time: span.endTime,
extra: { ...span.attributes },
error: span.status === SpanStatus.ERROR ? span.statusMessage : undefined,
inputs: span.attributes["input"],
outputs: span.attributes["output"],
tags: SpanSerializer.extractTags(span.attributes),
};
}
/**
* Serialize span for OpenTelemetry format
*/
static toOtelFormat(span) {
return {
traceId: span.traceId,
spanId: span.spanId,
parentSpanId: span.parentSpanId,
name: span.name,
kind: 1, // SPAN_KIND_INTERNAL
startTimeUnixNano: new Date(span.startTime).getTime() * 1_000_000,
endTimeUnixNano: span.endTime
? new Date(span.endTime).getTime() * 1_000_000
: undefined,
attributes: Object.entries(span.attributes)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => ({
key,
value: SpanSerializer.toOtelAttributeValue(value),
})),
status: {
code: span.status,
message: span.statusMessage,
},
events: span.events.map((e) => ({
name: e.name,
timeUnixNano: new Date(e.timestamp).getTime() * 1_000_000,
attributes: e.attributes
? Object.entries(e.attributes).map(([k, v]) => ({
key: k,
value: SpanSerializer.toOtelAttributeValue(v),
}))
: [],
})),
};
}
/**
* Convert value to OTel attribute value format
*/
static toOtelAttributeValue(value) {
if (typeof value === "string") {
return { stringValue: value };
}
if (typeof value === "number") {
return Number.isInteger(value)
? { intValue: value }
: { stringValue: String(value) };
}
if (typeof value === "boolean") {
return { boolValue: value };
}
return { stringValue: JSON.stringify(value) };
}
/**
* Map NeuroLink span type to LangSmith run type
*/
static mapSpanTypeToLangSmithRunType(type) {
const mapping = {
[SpanType.AGENT_RUN]: "chain",
[SpanType.WORKFLOW_STEP]: "chain",
[SpanType.TOOL_CALL]: "tool",
[SpanType.MODEL_GENERATION]: "llm",
[SpanType.EMBEDDING]: "embedding",
[SpanType.RETRIEVAL]: "retriever",
[SpanType.MEMORY]: "chain",
[SpanType.CONTEXT_COMPACTION]: "chain",
[SpanType.RAG]: "retriever",
[SpanType.EVALUATION]: "chain",
[SpanType.MCP_TRANSPORT]: "tool",
[SpanType.MEDIA_GENERATION]: "llm",
[SpanType.PPT_GENERATION]: "chain",
[SpanType.WORKFLOW]: "chain",
[SpanType.TTS]: "chain",
[SpanType.STT]: "chain",
[SpanType.SERVER_REQUEST]: "chain",
[SpanType.CUSTOM]: "chain",
};
return mapping[type] || "chain";
}
/**
* Extract tags from span attributes for LangSmith
*/
static extractTags(attributes) {
const tags = [];
if (attributes["ai.provider"]) {
tags.push(`provider:${attributes["ai.provider"]}`);
}
if (attributes["ai.model"]) {
tags.push(`model:${attributes["ai.model"]}`);
}
if (attributes["deployment.environment"]) {
tags.push(`env:${attributes["deployment.environment"]}`);
}
if (attributes["tool.name"]) {
tags.push(`tool:${attributes["tool.name"]}`);
}
return tags;
}
/**
* Create a generation span with AI-specific attributes
*/
static createGenerationSpan(params) {
return SpanSerializer.createSpan(SpanType.MODEL_GENERATION, params.name ?? `gen_ai.${params.provider}.chat`, {
"ai.provider": params.provider,
"ai.model": params.model,
"ai.temperature": params.temperature,
"ai.max_tokens": params.maxTokens,
input: params.input,
"user.id": params.userId,
"session.id": params.sessionId,
}, params.parentSpanId, params.traceId);
}
/**
* Create a tool call span
*/
static createToolCallSpan(params) {
return SpanSerializer.createSpan(SpanType.TOOL_CALL, `tool.${params.toolName}`, {
"tool.name": params.toolName,
"tool.server": params.server,
input: params.input,
}, params.parentSpanId, params.traceId);
}
/**
* Enrich span with token usage
*/
static enrichWithTokenUsage(span, usage) {
return SpanSerializer.updateAttributes(span, {
"ai.tokens.input": usage.promptTokens ?? 0,
"ai.tokens.output": usage.completionTokens ?? 0,
"ai.tokens.total": usage.totalTokens ??
(usage.promptTokens ?? 0) + (usage.completionTokens ?? 0),
"ai.tokens.cache_creation": usage.cacheCreationTokens,
"ai.tokens.cache_read": usage.cacheReadTokens,
"ai.tokens.reasoning": usage.reasoningTokens,
});
}
/**
* Enrich span with cost information
*/
static enrichWithCost(span, cost) {
return SpanSerializer.updateAttributes(span, {
"ai.cost.input": cost.inputCost,
"ai.cost.output": cost.outputCost,
"ai.cost.total": cost.totalCost,
"ai.cost.currency": cost.currency ?? "USD",
});
}
}
// Safe metadata filtering imported from shared module to avoid duplication
import { filterSafeMetadata } from "./safeMetadata.js";
//# sourceMappingURL=spanSerializer.js.map