@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) • 9.41 kB
JavaScript
/**
* Span Processor
* Handles span processing before export - enrichment, filtering, and transformation
* Fills the 9% gap in pattern compliance
*/
/**
* No-op processor that passes spans through unchanged
*/
export class PassThroughProcessor {
name = "pass-through";
process(span) {
return span;
}
}
/**
* Attribute enrichment processor
* Adds additional attributes to spans based on configuration
*/
export class AttributeEnrichmentProcessor {
name = "attribute-enrichment";
staticAttributes;
dynamicAttributes;
constructor(config) {
this.staticAttributes = config.staticAttributes ?? {};
this.dynamicAttributes = config.dynamicAttributes ?? (() => ({}));
}
process(span) {
return {
...span,
attributes: {
...this.staticAttributes,
...span.attributes,
...this.dynamicAttributes(span),
},
};
}
}
/**
* Filter processor - drops spans based on conditions
*/
export class FilterProcessor {
name = "filter";
predicate;
constructor(predicate) {
this.predicate = predicate;
}
process(span) {
return this.predicate(span) ? span : null;
}
}
/**
* Redaction processor - removes sensitive data from spans
*/
export class RedactionProcessor {
name = "redaction";
sensitiveKeys;
redactedValue;
constructor(config) {
this.sensitiveKeys = new Set(config?.sensitiveKeys ?? [
"api_key",
"apiKey",
"secret",
"password",
"token",
"authorization",
"credentials",
"private_key",
"privateKey",
"stack",
"error.stack",
]);
this.redactedValue = config?.redactedValue ?? "[REDACTED]";
}
process(span) {
const redactedAttributes = {};
for (const [key, value] of Object.entries(span.attributes)) {
const lowerKey = key.toLowerCase();
const isSensitive = Array.from(this.sensitiveKeys).some((sensitiveKey) => lowerKey.includes(sensitiveKey.toLowerCase()));
if (isSensitive && typeof value === "string") {
redactedAttributes[key] = this.redactedValue;
}
else if (typeof value === "object" && value !== null) {
redactedAttributes[key] = this.redactObject(value);
}
else {
redactedAttributes[key] = value;
}
}
return {
...span,
attributes: redactedAttributes,
};
}
redactObject(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.redactObject(item));
}
const result = {};
for (const [key, value] of Object.entries(obj)) {
const lowerKey = key.toLowerCase();
const isSensitive = Array.from(this.sensitiveKeys).some((sensitiveKey) => lowerKey.includes(sensitiveKey.toLowerCase()));
if (isSensitive && typeof value === "string") {
result[key] = this.redactedValue;
}
else if (typeof value === "object" && value !== null) {
result[key] = this.redactObject(value);
}
else {
result[key] = value;
}
}
return result;
}
}
/**
* Truncation processor - truncates large attribute values
*/
export class TruncationProcessor {
name = "truncation";
maxStringLength;
maxArrayLength;
constructor(config) {
this.maxStringLength = config?.maxStringLength ?? 10000;
this.maxArrayLength = config?.maxArrayLength ?? 100;
}
process(span) {
return {
...span,
attributes: this.truncateAttributes(span.attributes),
};
}
truncateAttributes(attrs) {
const result = {};
for (const [key, value] of Object.entries(attrs)) {
result[key] = this.truncateValue(value);
}
return result;
}
truncateValue(value) {
if (typeof value === "string" && value.length > this.maxStringLength) {
return value.substring(0, this.maxStringLength) + "...[truncated]";
}
if (Array.isArray(value) && value.length > this.maxArrayLength) {
return [
...value.slice(0, this.maxArrayLength),
`...[${value.length - this.maxArrayLength} more items]`,
];
}
if (typeof value === "object" && value !== null) {
const result = {};
for (const [k, v] of Object.entries(value)) {
result[k] = this.truncateValue(v);
}
return result;
}
return value;
}
}
/**
* Composite processor - chains multiple processors together
*/
export class CompositeProcessor {
name = "composite";
processors;
constructor(processors) {
this.processors = processors;
}
process(span) {
let result = span;
for (const processor of this.processors) {
if (result === null) {
return null;
}
result = processor.process(result);
}
return result;
}
async processAsync(span) {
let result = span;
for (const processor of this.processors) {
if (result === null) {
return null;
}
if (processor.processAsync) {
result = await processor.processAsync(result);
}
else {
result = processor.process(result);
}
}
return result;
}
async shutdown() {
await Promise.all(this.processors.map((p) => p.shutdown ? p.shutdown() : Promise.resolve()));
}
}
/**
* Batch processor - collects spans and processes them in batches
*/
export class BatchProcessor {
name = "batch";
innerProcessor;
batchSize;
flushIntervalMs;
batch = [];
flushTimer = null;
onBatchReady;
constructor(config) {
this.innerProcessor = config.processor ?? new PassThroughProcessor();
this.batchSize = config.batchSize ?? 100;
this.flushIntervalMs = config.flushIntervalMs ?? 5000;
this.onBatchReady = config.onBatchReady;
this.startFlushTimer();
}
process(span) {
const processed = this.innerProcessor.process(span);
if (processed) {
this.batch.push(processed);
if (this.batch.length >= this.batchSize) {
this.flush();
}
}
return processed;
}
startFlushTimer() {
this.flushTimer = setInterval(() => {
this.flush();
}, this.flushIntervalMs);
}
// Note: flush() is intentionally synchronous. The onBatchReady callback is
// typed as `(spans: SpanData[]) => void` — callers must not pass async
// exporters. If async export is needed, the callback should handle its own
// error reporting (e.g. fire-and-forget with promise error handlers).
flush() {
if (this.batch.length > 0 && this.onBatchReady) {
const spans = [...this.batch];
try {
this.onBatchReady(spans);
this.batch = [];
}
catch (flushError) {
// Keep spans for next flush attempt, but cap backlog growth
void flushError; // acknowledged — error is expected during exporter outages
const maxBacklog = this.batchSize * 20;
if (this.batch.length > maxBacklog) {
this.batch = this.batch.slice(this.batch.length - maxBacklog);
}
}
}
}
async shutdown() {
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = null;
}
this.flush();
if (this.innerProcessor.shutdown) {
await this.innerProcessor.shutdown();
}
}
}
/**
* Factory for creating span processors
*/
export class SpanProcessorFactory {
/**
* Create a standard processor pipeline for production use
*/
static createProductionPipeline(config) {
const processors = [
// Add service context
new AttributeEnrichmentProcessor({
staticAttributes: {
"service.name": config?.serviceName ?? "neurolink",
"deployment.environment": config?.environment ?? "production",
},
}),
// Redact sensitive data
new RedactionProcessor(),
// Truncate large values
new TruncationProcessor(),
];
if (config?.additionalProcessors) {
processors.push(...config.additionalProcessors);
}
return new CompositeProcessor(processors);
}
/**
* Create a minimal processor pipeline for development
*/
static createDevelopmentPipeline() {
return new CompositeProcessor([
new AttributeEnrichmentProcessor({
staticAttributes: {
"deployment.environment": "development",
},
}),
]);
}
}