neatlogs
v1.1.6
Published
AI agent debugging, collaboration, and trace observability. Built for teams using CrewAI, OpenAI, and more.
Maintainers
Readme
neatlogs
OpenTelemetry-native observability for LLM applications — TypeScript SDK.
Automatically trace LLM calls, agent workflows, tool invocations, and retrieval pipelines. Ship production-ready observability with a few lines of code.
Quick Start
import { init, span, shutdown } from 'neatlogs';
import OpenAI from 'openai';
async function main() {
// 1. Initialize the SDK
await init({
apiKey: process.env.NEATLOGS_API_KEY,
instrumentations: ['openai'],
});
// 2. Create your LLM client AFTER init()
const client = new OpenAI();
// 3. Wrap functions with span() for observability
const myWorkflow = span({ kind: 'WORKFLOW', name: 'qa-bot' }, async (query: string) => {
const res = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: query }],
});
return res.choices[0].message.content;
});
const answer = await myWorkflow('What is TypeScript?');
console.log(answer);
await shutdown();
}
main().catch(console.error);Installation
npm install neatlogsFor auto-instrumentation of specific LLM providers, install the corresponding peer dependency:
# OpenAI
npm install @arizeai/openinference-instrumentation-openai
# Anthropic
npm install @arizeai/openinference-instrumentation-anthropic
# AWS Bedrock
npm install @arizeai/openinference-instrumentation-bedrock
# LangChain
npm install @arizeai/openinference-instrumentation-langchain
# MCP (Model Context Protocol)
npm install @arizeai/openinference-instrumentation-mcp
# BeeAI
npm install @arizeai/openinference-instrumentation-beeai
# Claude Agent SDK
npm install @arizeai/openinference-instrumentation-claude-agent-sdk
# Google GenAI (@google/genai)
npm install @google/genaiCore Concepts
| Function | Purpose |
|----------|---------|
| init() | Initialize the SDK — sets up OTel providers, exporters, and instrumentation |
| span() | Wrap a function with observability — captures inputs, outputs, timing, and errors |
| trace() | Create a manual span with prompt template tracking and multi-turn session support |
| log() | Capture timestamped log steps within the active trace |
| shutdown() | Flush all pending data and shut down the SDK gracefully |
Important: Initialization Order
init() is async and must be called before creating any LLM client instances. This is because instrumentation works by monkey-patching libraries at init time.
// ✅ Correct
await init({ instrumentations: ['openai'] });
const client = new OpenAI(); // patched
// ❌ Wrong — client created before patching
const client = new OpenAI(); // NOT patched
await init({ instrumentations: ['openai'] });Important: No Top-Level Await
Always wrap your code in an async function main() pattern:
async function main() {
await init({ ... });
// ... your code
await shutdown();
}
main().catch(console.error);API Reference
init(options?)
Initialize the Neatlogs SDK. Returns Promise<void>.
await init({
apiKey: process.env.NEATLOGS_API_KEY,
instrumentations: ['openai', 'anthropic'],
debug: true,
});InitOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | process.env.NEATLOGS_API_KEY | Neatlogs API key. Export disabled if not set. |
| baseUrl | string | 'https://app.neatlogs.com' | Base URL for the Neatlogs API. |
| workflowName | string | Derived from process.argv[1] | Name of the workflow being traced. |
| sessionId | string | — | Explicit session ID for grouping traces. |
| autoSession | boolean | false | Auto-generate a session ID if none provided. |
| userId | string | — | User identifier for the session. |
| tags | string[] | — | Tags attached to all spans. |
| metadata | Record<string, any> | — | Custom metadata attached to all spans. |
| debug | boolean | false | Enable debug logging. |
| disableExport | boolean | false | Disable export to Neatlogs backend. |
| instrumentations | string[] | — | Libraries to auto-instrument (e.g., ['openai']). |
| mask | MaskFunction | — | Global mask function applied to all spans. |
| sampleRate | number | 1.0 | Sampling rate (0.0 to 1.0). |
| captureLogs | boolean | false | Capture log records via OTel LoggerProvider. |
| traceContent | boolean | true | Capture input/output content on spans. |
| pii | 'redact' | 'hash' | false | — | PII detection mode. |
| endpoint | string | 'https://ingest.neatlogs.com' | Base ingest endpoint. The SDK sends traces to /v1/traces and logs to /v1/logs. |
| batchSize | number | 100 | Maximum spans per export batch. |
| flushInterval | number | 5 | Seconds between batch flushes. |
| piiEnabled | boolean | — | Override team-level PII redaction toggle. |
| piiSpanTypes | string[] | — | Override which span types have server-side PII redaction. |
span(options, fn)
Wrap a function with OpenTelemetry span instrumentation. Returns a new function with the same signature that automatically creates a span when called.
const myFn = span({ kind: 'WORKFLOW', name: 'my-workflow' }, async (input: string) => {
return await process(input);
});
const result = await myFn('hello');The span() function is a higher-order function: it takes your function and returns a new, instrumented version. The returned function has the same arguments and return type as the original.
SpanOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| kind | SpanKind | — | Required. The kind of span. |
| name | string | Function name | Custom name for the span. |
| captureInput | boolean | true | Capture function input. |
| captureOutput | boolean | true | Capture function output. |
| captureStdout | boolean | false | Capture stdout during execution. |
| tags | string[] | — | Tags for this span. |
| metadata | Record<string, any> | — | Custom metadata for this span. |
| mask | MaskFunction | — | Per-span mask function. |
| internal | boolean | — | Mark span as internal (not user-facing). |
| role | string | — | Agent role (for kind: 'AGENT'). |
| goal | string | — | Agent goal (for kind: 'AGENT'). |
| toolName | string | — | Tool name (for kind: 'TOOL'). |
| parameters | Record<string, any> | — | Tool parameters schema (for kind: 'TOOL'). |
| model | string | — | Embedding model name (for kind: 'EMBEDDING'). |
| dimension | number | — | Embedding dimension (for kind: 'EMBEDDING'). |
SpanKind Values
| Kind | Use For |
|------|---------|
| WORKFLOW | Top-level orchestration / pipelines |
| AGENT | Autonomous agents with roles and goals |
| CHAIN | Sequential processing steps |
| TOOL | External tool calls (APIs, databases, etc.) |
| RETRIEVER | Document / vector retrieval |
| EMBEDDING | Vector embedding operations |
| MCP_TOOL | Model Context Protocol tool calls |
| GUARDRAIL | Safety checks and content filters |
Span() Decorator
TC39 Stage 3 class-method decorator for instrumenting class methods.
class MyAgent {
@Span({ kind: 'AGENT', role: 'researcher' })
async run(query: string) {
// automatically traced
return await this.search(query);
}
@Span({ kind: 'TOOL', name: 'web-search' })
async search(query: string) {
return { results: ['...'] };
}
}Note: Requires TypeScript 5.0+ with
"experimentalDecorators": false(the new TC39 Stage 3 decorators, not legacy decorators).
trace(options, fn)
Create a manual span that runs a callback. Unlike span(), which wraps a reusable function, trace() executes inline and is ideal for:
- Prompt template tracking — associate
PromptTemplateinstances with spans - Multi-turn sessions — automatically creates root traces when
sessionIdis set - Grouping operations — wrap a block of code in an ad-hoc span
const result = await trace({
name: 'llm-call',
promptTemplate: myTemplate,
}, async (activeSpan) => {
const rendered = myTemplate.compile({ name: 'world' });
return await callLLM(rendered);
});TraceOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| name | string | — | Required. Name for the trace span. |
| kind | SpanKind | 'CHAIN' | Span kind. |
| promptTemplate | string | PromptTemplate | — | Prompt template to track. |
| promptVariables | Record<string, any> | — | Prompt variables for the template. |
| userPromptTemplate | string | UserPromptTemplate | — | User prompt template. |
| userPromptVariables | Record<string, any> | — | User prompt variables. |
| version | string | — | Prompt version identifier. |
| captureStdout | boolean | false | Capture stdout during execution. |
| mask | MaskFunction | — | Per-trace mask function. |
| attributes | Record<string, any> | — | Custom attributes on the span. |
| tags | string[] | — | Tags for this trace. |
| metadata | Record<string, any> | — | Custom metadata. |
span() vs trace()
| | span() | trace() |
|---|----------|-----------|
| Pattern | Higher-order function wrapper | Inline callback |
| Reuse | Returns a reusable function | Executes immediately |
| Prompt tracking | No | Yes — promptTemplate, promptVariables |
| Session-aware | No | Yes — creates root traces for multi-turn sessions |
| Best for | Wrapping functions/methods | Ad-hoc tracing blocks, prompt versioning |
log(template, options?)
Capture a timestamped log step within the current trace. Uses {key} placeholders for template variables.
log('Processing query: {query}', { query: 'What is TypeScript?' });
log('Retrieved {count} documents in {ms}ms', { count: 5, ms: 120 });
log('Classification result', { category: 'technical', level: 'debug' });Requires captureLogs: true in init(). Log records are emitted as OTel LogRecords associated with the active span and exported to the OTLP logs endpoint at /v1/logs.
The special level key sets the log severity ('info', 'debug', 'warn', 'error'). All other keys are template variables and are also recorded as log.{key} attributes.
PromptTemplate / UserPromptTemplate
Template classes for prompt versioning with {{variable}} placeholders. When used with trace(), variables are automatically captured on the span for prompt tracking.
// String template
const systemPrompt = new PromptTemplate(
'You are a {{role}} assistant specializing in {{topic}}.'
);
// Message array template
const chatPrompt = new PromptTemplate([
{ role: 'system', content: 'You are a {{role}} assistant.' },
{ role: 'user', content: '{{question}}' },
]);
// Compile with variables
const rendered = systemPrompt.compile({ role: 'helpful', topic: 'TypeScript' });
// => 'You are a helpful assistant specializing in TypeScript.'
// Access template metadata
systemPrompt.variables; // ['role', 'topic']
systemPrompt.template; // raw template stringUserPromptTemplate is identical but stores context separately — use it for the user/human turn in multi-template setups:
const systemTpl = new PromptTemplate('You are a {{role}} assistant.');
const userTpl = new UserPromptTemplate('{{question}}');
await trace({
name: 'qa',
promptTemplate: systemTpl,
userPromptTemplate: userTpl,
}, async () => {
const system = systemTpl.compile({ role: 'helpful' });
const user = userTpl.compile({ question: 'What is TypeScript?' });
// Variables from both templates are captured on the span
});PromptClient
Server-side prompt management for storing, versioning, and retrieving prompts from the Neatlogs backend.
import { PromptClient } from 'neatlogs';
const client = new PromptClient({
baseUrl: 'https://app.neatlogs.com',
apiKey: process.env.NEATLOGS_API_KEY!,
});
// Create a prompt
const prompt = await client.createPrompt({
name: 'qa-system',
content: 'You are a {{role}} assistant for {{company}}.',
labels: ['production'],
});
// Fetch by name (returns latest version)
const handle = await client.getPrompt('qa-system');
// Fetch by label or version
const prod = await client.getPrompt('qa-system', { label: 'production' });
const v2 = await client.getPrompt('qa-system', { version: 2 });
// Compile with variables
const rendered = handle.compile({ role: 'helpful', company: 'Acme' });
// Compile as message array
const messages = handle.compileMessages({ role: 'helpful', company: 'Acme' });
// List all prompts
const all = await client.listPrompts();
// Update prompt content
await client.updatePrompt('qa-system', { content: 'Updated: {{role}} for {{company}}.' });
// Save a new version
await client.saveAsVersion('qa-system', { label: 'v2' });
// Delete a prompt
await client.deletePrompt('qa-system');Module-level convenience functions are also available after init():
import { init, getPrompt, fetchPrompt, listPrompts, createPrompt, updatePrompt, saveAsVersion, deletePrompt, removeTag } from 'neatlogs';
await init({ apiKey: process.env.NEATLOGS_API_KEY });
const handle = await getPrompt('my-prompt');
const rendered = handle.compile({ name: 'world' });flush() / shutdown()
// Flush pending spans without shutting down
await flush();
// Flush and shut down — call before process exit
await shutdown();shutdown() resets all SDK state so init() can be called again if needed.
bindTemplates(llm, systemTpl, userTpl?, compiledVars?)
Bind prompt templates to a LangChain-compatible LLM so templates are automatically captured on LLM spans managed by frameworks like CrewAI.
import { bindTemplates, PromptTemplate, UserPromptTemplate } from 'neatlogs';
const systemTpl = new PromptTemplate('You are a {{role}} assistant.');
const userTpl = new UserPromptTemplate('Research: {{topic}}');
const boundLlm = bindTemplates(llm, systemTpl, userTpl, { topic: 'AI safety' });
// Pass boundLlm to your framework — template context is injected on every invoke()registerCrewaiTask(taskId, taskDescription)
Register a CrewAI task for automatic span annotation.
import { registerCrewaiTask } from 'neatlogs';
registerCrewaiTask('research-task', 'Research the latest AI developments');Supported Instrumentations
Auto-Instrumented (via OpenInference)
These libraries are automatically instrumented when listed in instrumentations:
| Library | Package | Instrumentation |
|---------|---------|-----------------|
| openai | openai | @arizeai/openinference-instrumentation-openai |
| anthropic | @anthropic-ai/sdk | @arizeai/openinference-instrumentation-anthropic |
| bedrock | @aws-sdk/client-bedrock-runtime | @arizeai/openinference-instrumentation-bedrock |
| langchain | @langchain/core | @arizeai/openinference-instrumentation-langchain |
| mcp | @modelcontextprotocol/sdk | @arizeai/openinference-instrumentation-mcp |
| beeai | beeai-framework | @arizeai/openinference-instrumentation-beeai |
| claude_agent_sdk | @anthropic-ai/claude-agent-sdk | @arizeai/openinference-instrumentation-claude-agent-sdk |
Custom Instrumentors (built into neatlogs)
| Library | Package | Notes |
|---------|---------|-------|
| google_genai | @google/genai | Custom neatlogs instrumentor |
| crewai | crewai | Custom neatlogs instrumentor; auto-loads litellm |
Registry Entries (not yet instrumented in TypeScript)
The following libraries are registered in the instrumentation registry for future support. Passing them to instrumentations will log a debug message and skip gracefully:
cohere, groq, together, vertexai, google_generativeai, mistralai, ollama, watsonx, alephalpha, replicate, sagemaker, huggingface_hub, litellm, langgraph, llamaindex, autogen, haystack, dspy, chromadb, pinecone, weaviate, qdrant, milvus, opensearch, elasticsearch, redis, marqo, instructor, guardrails, google_adk, agno, openai_agents, pydantic_ai, smolagents, strands, pipecat, portkey, promptflow
Framework Integrations
For frameworks that don't fit the auto-instrument-on-init pattern, neatlogs ships dedicated companion packages. Install only the ones you need:
| Framework | Package | Helper |
|-----------|---------|--------|
| Mastra (@mastra/core) | @neatlogs/instrumentation-mastra | getMastraObservability() — pass to new Mastra({ observability }) |
| Vercel AI SDK (ai) | @neatlogs/instrumentation-ai-sdk | getAISDKWrapper() — wraps generateText / streamText / generateObject / streamObject |
// Vercel AI SDK
import { init, getAISDKWrapper, shutdown } from 'neatlogs';
import * as ai from 'ai';
import { openai } from '@ai-sdk/openai';
await init({ apiKey: process.env.NEATLOGS_API_KEY });
const wrapAISDK = await getAISDKWrapper();
const { generateText } = wrapAISDK(ai);
const { text } = await generateText({
model: openai('gpt-4o-mini'),
prompt: 'What is TypeScript?',
});
await shutdown();Configuration
Environment Variables
| Variable | Description |
|----------|-------------|
| NEATLOGS_API_KEY | API key (fallback when apiKey option is not provided) |
| NEATLOGS_DISABLE_EXPORT | Set to true, 1, or yes to disable export |
Programmatic Configuration
All configuration is passed via init() options. See the InitOptions table above.
await init({
apiKey: process.env.NEATLOGS_API_KEY,
workflowName: 'my-pipeline',
sessionId: 'session-123',
userId: 'user-456',
tags: ['production', 'v2'],
metadata: { environment: 'prod' },
instrumentations: ['openai', 'anthropic'],
sampleRate: 0.5,
captureLogs: true,
debug: true,
});PII Masking
Global Mask
Apply a mask function to all spans:
await init({
apiKey: process.env.NEATLOGS_API_KEY,
mask: (spanData) => {
// Redact email addresses
for (const [key, value] of Object.entries(spanData)) {
if (typeof value === 'string') {
spanData[key] = value.replace(/[\w.-]+@[\w.-]+/g, '[REDACTED]');
}
}
return spanData;
},
});Per-Span Mask
Apply a mask to a specific span:
const sensitive = span({
kind: 'TOOL',
name: 'user-lookup',
mask: (spanData) => {
delete spanData['input.value'];
return spanData;
},
}, async (userId: string) => {
return await lookupUser(userId);
});Per-Trace Mask
await trace({
name: 'sensitive-operation',
mask: (spanData) => {
// Return null to drop the span entirely
return null;
},
}, async () => {
// This span will not be exported
});Server-Side PII Redaction
await init({
apiKey: process.env.NEATLOGS_API_KEY,
pii: 'redact', // or 'hash' or false
piiEnabled: true, // override team-level toggle
piiSpanTypes: ['LLM'], // only redact LLM spans
});Examples
See the examples/ directory for complete, runnable examples:
| File | Description |
|------|-------------|
| basic-openai.ts | Basic OpenAI usage with auto-instrumentation |
| prompt-management.ts | PromptTemplate + trace() for prompt versioning |
| multi-agent-workflow.ts | Nested spans: WORKFLOW → AGENT → TOOL |
| custom-spans.ts | All span kinds: WORKFLOW, CHAIN, AGENT, TOOL, RETRIEVER, EMBEDDING, GUARDRAIL |
| sdk_examples/ai_sdk_basic/ | Vercel AI SDK via wrapAISDK — generateText + streamText + tools |
Run any example with:
NEATLOGS_API_KEY=your-key npx tsx examples/basic-openai.tsLicense
MIT
