This comprehensive guide covers integrating OwlMetric with Node.js applications for AI cost tracking and analytics. We'll cover Express.js, Fastify, and other popular Node.js frameworks.
The Direct SDK provides the best performance and flexibility for Node.js applications.
npm install @owlmetric/tracker
// server.js
import express from 'express';
import OpenAI from 'openai';
import { createTrackedClient } from '@owlmetric/tracker';
const app = express();
app.use(express.json());
// Create tracked OpenAI client
const openai = createTrackedClient(OpenAI, {
apiKey: process.env.OPENAI_API_KEY,
owlmetricToken: process.env.OWLMETRIC_OPENAI_TOKEN,
});
app.post('/api/chat', async (req, res) => {
const { messages } = req.body;
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages,
stream: true,
});
res.setHeader('Content-Type', 'text/plain');
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content || '';
res.write(content);
}
res.end();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
For comprehensive observability with automatic telemetry tracking.
npm install @owlmetric/tracker @opentelemetry/sdk-node @opentelemetry/sdk-logs @opentelemetry/api-logs
// server.js
import express from 'express';
import { OwlMetricTraceLogger } from '@owlmetric/tracker/owlmetric_trace_logger';
import { createOpenAI } from '@ai-sdk/openai';
import { generateText, streamText } from 'ai';
// Initialize OpenTelemetry logging
const logger = new OwlMetricTraceLogger();
logger.init();
const app = express();
app.use(express.json());
const ai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
app.post('/api/chat', async (req, res) => {
const { messages } = req.body;
try {
const { textStream } = streamText({
model: ai('gpt-4'),
messages,
experimental_telemetry: {
isEnabled: true,
metadata: {
xOwlToken: process.env.OWLMETRIC_TOKEN,
userId: req.headers['x-user-id'],
sessionId: req.headers['x-session-id'],
},
},
});
res.setHeader('Content-Type', 'text/plain');
for await (const textPart of textStream) {
res.write(textPart);
}
res.end();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000);
For existing applications with minimal code changes.
// server.js
import express from 'express';
import OpenAI from 'openai';
const app = express();
app.use(express.json());
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: 'https://owlmetric.com/api/proxy',
defaultHeaders: {
'x-owlmetric': process.env.OWLMETRIC_API_KEY,
},
});
app.post('/api/chat', async (req, res) => {
const { messages } = req.body;
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages,
});
res.json(completion);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000);
// app.js
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import OpenAI from 'openai';
import Anthropic from '@anthropic-ai/sdk';
import { createTrackedClient } from '@owlmetric/tracker';
const app = express();
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Rate limiting
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
});
app.use('/api/', limiter);
// Tracked AI clients
const openai = createTrackedClient(OpenAI, {
apiKey: process.env.OPENAI_API_KEY,
owlmetricToken: process.env.OWLMETRIC_OPENAI_TOKEN,
});
const anthropic = createTrackedClient(Anthropic, {
apiKey: process.env.ANTHROPIC_API_KEY,
owlmetricToken: process.env.OWLMETRIC_ANTHROPIC_TOKEN,
});
// Multi-provider chat endpoint
app.post('/api/chat', async (req, res) => {
const { messages, provider = 'openai', model, stream = false } = req.body;
try {
let completion;
switch (provider) {
case 'openai':
completion = await openai.chat.completions.create({
model: model || 'gpt-4',
messages,
stream,
});
break;
case 'anthropic':
if (stream) {
completion = await anthropic.messages.create({
model: model || 'claude-3-sonnet-20240229',
max_tokens: 1000,
messages,
stream: true,
});
} else {
completion = await anthropic.messages.create({
model: model || 'claude-3-sonnet-20240229',
max_tokens: 1000,
messages,
});
}
break;
default:
return res.status(400).json({ error: 'Unsupported provider' });
}
if (stream) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
if (provider === 'openai') {
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content || '';
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
} else if (provider === 'anthropic') {
for await (const event of completion) {
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
res.write(`data: ${JSON.stringify({ content: event.delta.text })}\n\n`);
}
}
}
res.write('data: [DONE]\n\n');
res.end();
} else {
res.json(completion);
}
} catch (error) {
console.error('AI API Error:', error);
res.status(500).json({ error: error.message });
}
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// server.js
import Fastify from 'fastify';
import OpenAI from 'openai';
import { createTrackedClient } from '@owlmetric/tracker';
const fastify = Fastify({ logger: true });
// Register CORS
await fastify.register(import('@fastify/cors'));
// Register rate limit
await fastify.register(import('@fastify/rate-limit'), {
max: 10,
timeWindow: '1 minute',
});
// Tracked OpenAI client
const openai = createTrackedClient(OpenAI, {
apiKey: process.env.OPENAI_API_KEY,
owlmetricToken: process.env.OWLMETRIC_OPENAI_TOKEN,
});
// Schema definitions
const chatSchema = {
body: {
type: 'object',
required: ['messages'],
properties: {
messages: {
type: 'array',
items: {
type: 'object',
properties: {
role: { type: 'string' },
content: { type: 'string' },
},
},
},
model: { type: 'string' },
stream: { type: 'boolean' },
},
},
};
// Chat endpoint
fastify.post('/api/chat', { schema: chatSchema }, async (request, reply) => {
const { messages, model = 'gpt-4', stream = false } = request.body;
try {
const completion = await openai.chat.completions.create({
model,
messages,
stream,
});
if (stream) {
reply.type('text/event-stream');
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content || '';
reply.raw.write(`data: ${JSON.stringify({ content })}\n\n`);
}
reply.raw.write('data: [DONE]\n\n');
reply.raw.end();
} else {
return completion;
}
} catch (error) {
reply.code(500).send({ error: error.message });
}
});
// Start server
const start = async () => {
try {
await fastify.listen({ port: 3000 });
console.log('Server running on port 3000');
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
// server.js
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
import OpenAI from 'openai';
import { createTrackedClient } from '@owlmetric/tracker';
const app = new Koa();
const router = new Router();
// Middleware
app.use(cors());
app.use(bodyParser());
// Tracked OpenAI client
const openai = createTrackedClient(OpenAI, {
apiKey: process.env.OPENAI_API_KEY,
owlmetricToken: process.env.OWLMETRIC_OPENAI_TOKEN,
});
// Chat endpoint
router.post('/api/chat', async (ctx) => {
const { messages, model = 'gpt-4' } = ctx.request.body;
try {
const completion = await openai.chat.completions.create({
model,
messages,
});
ctx.body = completion;
} catch (error) {
ctx.status = 500;
ctx.body = { error: error.message };
}
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000, () => {
console.log('Server running on port 3000');
});
// lib/ai-providers.js
import OpenAI from 'openai';
import Anthropic from '@anthropic-ai/sdk';
import { GoogleGenAI } from '@google/genai';
import { createTrackedClient } from '@owlmetric/tracker';
class AIProviderManager {
constructor() {
this.providers = {
openai: createTrackedClient(OpenAI, {
apiKey: process.env.OPENAI_API_KEY,
owlmetricToken: process.env.OWLMETRIC_OPENAI_TOKEN,
}),
anthropic: createTrackedClient(Anthropic, {
apiKey: process.env.ANTHROPIC_API_KEY,
owlmetricToken: process.env.OWLMETRIC_ANTHROPIC_TOKEN,
}),
google: createTrackedClient(GoogleGenAI, {
apiKey: process.env.GOOGLE_API_KEY,
owlmetricToken: process.env.OWLMETRIC_GOOGLE_TOKEN,
}),
};
}
async chat(provider, model, messages, options = {}) {
const client = this.providers[provider];
if (!client) {
throw new Error(`Provider ${provider} not supported`);
}
switch (provider) {
case 'openai':
return await client.chat.completions.create({
model,
messages,
...options,
});
case 'anthropic':
return await client.messages.create({
model,
max_tokens: options.max_tokens || 1000,
messages,
...options,
});
case 'google':
return await client.models.generateContent({
model,
contents: messages.map(m => m.content).join('\n'),
...options,
});
default:
throw new Error(`Provider ${provider} not implemented`);
}
}
async streamChat(provider, model, messages, options = {}) {
const client = this.providers[provider];
if (!client) {
throw new Error(`Provider ${provider} not supported`);
}
switch (provider) {
case 'openai':
return await client.chat.completions.create({
model,
messages,
stream: true,
...options,
});
case 'anthropic':
return await client.messages.create({
model,
max_tokens: options.max_tokens || 1000,
messages,
stream: true,
...options,
});
case 'google':
return await client.models.generateContentStream({
model,
contents: messages.map(m => m.content).join('\n'),
...options,
});
default:
throw new Error(`Provider ${provider} not implemented`);
}
}
}
export default new AIProviderManager();
// middleware/owlmetric-context.js
import { AsyncLocalStorage } from 'async_hooks';
const owlmetricContext = new AsyncLocalStorage();
export function createOwlMetricMiddleware() {
return (req, res, next) => {
const context = {
userId: req.headers['x-user-id'],
sessionId: req.headers['x-session-id'],
requestId: req.headers['x-request-id'] || crypto.randomUUID(),
userAgent: req.headers['user-agent'],
ip: req.ip,
timestamp: new Date().toISOString(),
};
owlmetricContext.run(context, () => {
next();
});
};
}
export function getOwlMetricContext() {
return owlmetricContext.getStore();
}
// Usage in route handler
import { getOwlMetricContext } from '../middleware/owlmetric-context.js';
app.post('/api/chat', async (req, res) => {
const context = getOwlMetricContext();
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: req.body.messages,
// Context is automatically included in tracking
});
res.json(completion);
});
// lib/ai-queue.js
import Bull from 'bull';
import aiProviders from './ai-providers.js';
const aiQueue = new Bull('AI Processing', {
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
},
});
aiQueue.process('chat', async (job) => {
const { provider, model, messages, options } = job.data;
try {
const result = await aiProviders.chat(provider, model, messages, options);
return result;
} catch (error) {
throw new Error(`AI processing failed: ${error.message}`);
}
});
export async function queueChatRequest(provider, model, messages, options = {}) {
const job = await aiQueue.add('chat', {
provider,
model,
messages,
options,
});
return job.id;
}
export async function getChatResult(jobId) {
const job = await aiQueue.getJob(jobId);
if (!job) {
throw new Error('Job not found');
}
if (job.finishedOn) {
return job.returnvalue;
} else if (job.failedReason) {
throw new Error(job.failedReason);
} else {
return { status: 'processing' };
}
}
export default aiQueue;
Create .env file:
# AI Provider API Keys
OPENAI_API_KEY=sk-your-openai-key
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key
GOOGLE_API_KEY=your-google-key
# OwlMetric Tracking Tokens (for Direct SDK)
OWLMETRIC_OPENAI_TOKEN=pt_your_openai_token
OWLMETRIC_ANTHROPIC_TOKEN=pt_your_anthropic_token
OWLMETRIC_GOOGLE_TOKEN=pt_your_google_token
# OwlMetric Proxy Key (for Proxy method)
OWLMETRIC_API_KEY=pk_your_project_key
# Server Configuration
PORT=3000
NODE_ENV=production
# Optional: Redis for queue processing
REDIS_HOST=localhost
REDIS_PORT=6379
// ecosystem.config.js
module.exports = {
apps: [{
name: 'ai-api-server',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
}],
};
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'
services:
ai-api:
build: .
ports:
- "3000:3000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- OWLMETRIC_OPENAI_TOKEN=${OWLMETRIC_OPENAI_TOKEN}
- NODE_ENV=production
restart: unless-stopped
redis:
image: redis:alpine
ports:
- "6379:6379"
restart: unless-stopped
// lib/logger.js
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'ai-api' },
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
export default logger;
// routes/health.js
import express from 'express';
import logger from '../lib/logger.js';
const router = express.Router();
router.get('/health', async (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.env.npm_package_version,
};
// Check AI provider connectivity
try {
// Simple test request to verify connectivity
await fetch('https://api.openai.com/v1/models', {
headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }
});
health.openai = 'connected';
} catch (error) {
health.openai = 'disconnected';
health.status = 'degraded';
}
logger.info('Health check performed', health);
res.json(health);
});
export default router;
// tests/ai-providers.test.js
import { jest } from '@jest/globals';
import aiProviders from '../lib/ai-providers.js';
// Mock the tracked clients
jest.mock('@owlmetric/tracker', () => ({
createTrackedClient: jest.fn((ClientClass, config) => {
return new ClientClass(config);
}),
}));
describe('AI Providers', () => {
test('should handle OpenAI chat request', async () => {
const messages = [
{ role: 'user', content: 'Hello' }
];
const result = await aiProviders.chat('openai', 'gpt-3.5-turbo', messages);
expect(result).toBeDefined();
expect(result.choices).toBeDefined();
});
test('should handle streaming requests', async () => {
const messages = [
{ role: 'user', content: 'Count to 3' }
];
const stream = await aiProviders.streamChat('openai', 'gpt-3.5-turbo', messages);
expect(stream).toBeDefined();
let chunks = 0;
for await (const chunk of stream) {
chunks++;
if (chunks > 10) break; // Prevent infinite loop in tests
}
expect(chunks).toBeGreaterThan(0);
});
});
// tests/integration/chat.test.js
import request from 'supertest';
import app from '../server.js';
describe('Chat API', () => {
test('POST /api/chat should return completion', async () => {
const response = await request(app)
.post('/api/chat')
.send({
messages: [
{ role: 'user', content: 'Say "test successful"' }
],
provider: 'openai',
model: 'gpt-3.5-turbo'
})
.expect(200);
expect(response.body.choices).toBeDefined();
expect(response.body.choices[0].message.content).toContain('test successful');
});
});
Error Handling:
Rate Limiting:
Security:
Performance:
Monitoring: