Comprehensive integration, configuration, and operational reference for @senzops/apm-node.
The SDK captures distributed traces, child spans, errors, logs, background task runs, and runtime metrics from Node.js services and sends them to Senzor. It replaces OpenTelemetry auto-instrumentation with a zero-dependency, Senzor-native alternative.
43 auto-instrumentations. 8 framework wrappers. Zero runtime dependencies.
- Getting Started
- Startup Modes
- Framework Integration Guide
- AWS Lambda Integration
- Cloudflare Workers Integration
- Auto-Instrumentation Reference
- AI / LLM SDK Instrumentation
- Database & Cache Instrumentation
- Messaging & Queue Instrumentation
- Cloud Provider Instrumentation
- gRPC, GraphQL & Network
- Log Correlation
- Background Task Monitoring
- Manual Traces & Spans
- Error Tracking
- Runtime Metrics
- Distributed Tracing & Context Propagation
- Configuration Reference
- Environment Variables
- Security, Privacy & Cardinality
- Transport Behavior
- Ingestion Payload Format
- Deployment Patterns
- Troubleshooting
- Public API Reference
- Build & Publish
npm install @senzops/apm-nodeimport Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });That's it. Once initialized, the SDK automatically instruments all supported libraries that your application imports. No additional configuration or per-library setup is required.
Preload mode gives the SDK the best coverage because it installs instrumentation hooks before the application imports any library.
CommonJS:
SENZOR_API_KEY=sz_apm_xxx node -r @senzops/apm-node/register server.jsESM:
SENZOR_API_KEY=sz_apm_xxx node --import @senzops/apm-node/register server.mjsDocker:
ENV SENZOR_API_KEY=sz_apm_xxx
CMD ["node", "-r", "@senzops/apm-node/register", "dist/server.js"]PM2:
{
"apps": [{
"name": "orders-api",
"script": "dist/server.js",
"node_args": "-r @senzops/apm-node/register",
"env": {
"SENZOR_API_KEY": "sz_apm_xxx",
"NODE_ENV": "production"
}
}]
}When preload flags are unavailable (some serverless platforms, custom runtimes), initialize at the very top of your entrypoint.
// init.ts — import this FIRST
import Senzor from '@senzops/apm-node';
Senzor.init({
apiKey: process.env.SENZOR_API_KEY!,
endpoint: process.env.SENZOR_ENDPOINT,
batchSize: 100,
flushInterval: 10000,
});
// Then import your app
import './server';| Scenario | Mode |
|---|---|
| Standard Node.js server (Express, Fastify, Koa, etc.) | Preload |
| Docker, Kubernetes, PM2 | Preload |
| Next.js API routes | Programmatic + wrapper |
| Nuxt / Nitro / H3 | Programmatic + wrapper |
| Cloudflare Workers | Programmatic + wrapper |
| AWS Lambda | Programmatic + wrapLambda, or Lambda Layer with preload |
Serverless without NODE_OPTIONS support |
Programmatic |
Preload mode captures inbound HTTP automatically. Add middleware for route detection and error capture:
import express from 'express';
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
const app = express();
// MUST be the first middleware
app.use(Senzor.requestHandler());
app.get('/users/:id', async (req, res) => {
res.json({ id: req.params.id });
});
// MUST be the last middleware
app.use(Senzor.errorHandler());
app.listen(3000);import Fastify from 'fastify';
import Senzor from '@senzops/apm-node';
const fastify = Fastify();
fastify.register(Senzor.fastifyPlugin, {
apiKey: process.env.SENZOR_API_KEY!,
});
fastify.get('/orders/:orderId', async (request) => {
return { orderId: request.params.orderId };
});
await fastify.listen({ port: 3000 });NestJS runs on Express (default) or Fastify. For Express:
import Senzor from '@senzops/apm-node';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// Initialize BEFORE creating the Nest app
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(Senzor.requestHandler());
await app.listen(3000);
}
bootstrap();The NestJS instrumentation auto-captures:
- Controller and method resolution
- Guards, Interceptors, Pipes execution
- Exception filter processing
import Koa from 'koa';
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
const app = new Koa();
app.use(async (ctx) => {
ctx.body = { ok: true };
});
app.listen(3000);With preload mode, Koa middleware stack is automatically instrumented.
import Hapi from '@hapi/hapi';
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
const server = Hapi.server({ port: 3000 });
server.route({
method: 'GET',
path: '/users/{id}',
handler: (request) => ({ id: request.params.id }),
});
await server.start();import restify from 'restify';
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
const server = restify.createServer();
server.get('/users/:id', (req, res, next) => {
res.send({ id: req.params.id });
next();
});
server.listen(3000);App Router:
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
export const GET = Senzor.wrapNextRoute(async (request: Request) => {
return Response.json({ ok: true });
});Pages Router:
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
export default Senzor.wrapNextPages(async (req, res) => {
res.status(200).json({ ok: true });
});import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
export default Senzor.wrapH3(defineEventHandler(async (event) => {
return { ok: true };
}));Nitro Plugin (for Cloudflare Workers preset):
// server/plugins/senzor.ts
import { Senzor } from '@senzops/apm-node';
export default defineNitroPlugin((nitroApp) => {
Senzor.init({ apiKey: '<YOUR_APM_KEY>' });
Senzor.nitroPlugin(nitroApp);
});import http from 'http';
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
});
server.listen(3000);With preload mode, no wrapper is needed — http.createServer is automatically instrumented.
Three deployment methods, from zero-code to code-level:
| Method | Code Changes | Setup | Coverage |
|---|---|---|---|
| Extension Layer (recommended) | None | Add layer + set env vars | Full: auto-handler wrapping + all auto-instrumentation |
| Handler Wrapper | Modify handler file | npm install + code change |
Full |
| Preload Layer | None | Add layer + NODE_OPTIONS |
Partial: outgoing calls/DB only, no handler wrapping |
The Extension Layer approach works identically to New Relic and Datadog Lambda Layers. You package @senzops/apm-node as a Lambda Layer, point the function's handler to Senzor's auto-wrapper, and set SENZOR_LAMBDA_HANDLER to your original handler. No code changes.
How it works:
- Lambda invokes
@senzops/apm-node/dist/lambda-handler.handler - The auto-wrapper reads
SENZOR_LAMBDA_HANDLER(e.g.,index.handler) - It dynamically loads your original handler module from
LAMBDA_TASK_ROOT - It wraps your handler with
wrapLambda()for full APM coverage - It re-exports the wrapped function for Lambda to invoke
Step 1: Build the Lambda Layer
mkdir -p senzor-layer/nodejs
cd senzor-layer/nodejs
npm init -y
npm install @senzops/apm-node
cd ..
zip -r senzor-apm-layer.zip nodejs/Step 2: Publish the Layer
aws lambda publish-layer-version \
--layer-name senzor-apm-node \
--zip-file fileb://senzor-apm-layer.zip \
--compatible-runtimes nodejs18.x nodejs20.x nodejs22.xStep 3: Configure Your Function
aws lambda update-function-configuration \
--function-name my-function \
--layers <LAYER_ARN> \
--handler @senzops/apm-node/dist/lambda-handler.handler \
--environment Variables="{ \
SENZOR_API_KEY=sz_apm_xxx, \
SENZOR_LAMBDA_HANDLER=index.handler, \
NODE_OPTIONS=--require @senzops/apm-node/register \
}"| Environment Variable | Required | Description |
|---|---|---|
SENZOR_API_KEY |
Yes | Your Senzor APM API key |
SENZOR_LAMBDA_HANDLER |
Yes | Original handler path (e.g., index.handler, src/app.myHandler) |
NODE_OPTIONS |
Recommended | --require @senzops/apm-node/register for full preload coverage |
The handler path supports nested exports: SENZOR_LAMBDA_HANDLER=src/handlers.api.get resolves to require('src/handlers').api.get.
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as path from 'path';
// Create the Senzor APM Layer
const senzorLayer = new lambda.LayerVersion(this, 'SenzorApmLayer', {
code: lambda.Code.fromAsset(path.join(__dirname, 'senzor-layer')),
compatibleRuntimes: [
lambda.Runtime.NODEJS_18_X,
lambda.Runtime.NODEJS_20_X,
lambda.Runtime.NODEJS_22_X,
],
description: 'Senzor APM Node.js Lambda Extension Layer',
});
// Attach to your Lambda function
const fn = new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
// Point handler to Senzor's auto-wrapper
handler: '@senzops/apm-node/dist/lambda-handler.handler',
code: lambda.Code.fromAsset('lambda'),
layers: [senzorLayer],
environment: {
SENZOR_API_KEY: senzorApiKey.stringValue,
// Your original handler path
SENZOR_LAMBDA_HANDLER: 'index.handler',
NODE_OPTIONS: '--require @senzops/apm-node/register',
},
});Build the layer directory first:
mkdir -p senzor-layer/nodejs && cd senzor-layer/nodejs
npm init -y && npm install @senzops/apm-node# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Layers:
- !Ref SenzorApmLayer
Environment:
Variables:
SENZOR_API_KEY: !Ref SenzorApiKey
NODE_OPTIONS: '--require @senzops/apm-node/register'
Resources:
SenzorApmLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: senzor-apm-node
ContentUri: senzor-layer/
CompatibleRuntimes:
- nodejs18.x
- nodejs20.x
- nodejs22.x
MyFunction:
Type: AWS::Serverless::Function
Properties:
Handler: '@senzops/apm-node/dist/lambda-handler.handler'
Runtime: nodejs20.x
CodeUri: src/
Environment:
Variables:
SENZOR_LAMBDA_HANDLER: index.handler# serverless.yml
service: my-service
provider:
name: aws
runtime: nodejs20.x
environment:
SENZOR_API_KEY: ${ssm:/senzor/api-key}
NODE_OPTIONS: '--require @senzops/apm-node/register'
layers:
senzorApm:
path: senzor-layer
compatibleRuntimes:
- nodejs18.x
- nodejs20.x
- nodejs22.x
functions:
api:
handler: '@senzops/apm-node/dist/lambda-handler.handler'
layers:
- !Ref SenzorApmLambdaLayer
environment:
SENZOR_LAMBDA_HANDLER: src/handlers/api.handler-
Create the Layer zip locally:
mkdir -p senzor-layer/nodejs && cd senzor-layer/nodejs npm init -y && npm install @senzops/apm-node cd .. && zip -r senzor-apm-layer.zip nodejs/
-
Upload the Layer: Go to Lambda > Layers > Create layer. Upload
senzor-apm-layer.zip. Set compatible runtimes tonodejs18.x,nodejs20.x,nodejs22.x. -
Attach to your function: Go to your Lambda function > Layers > Add a layer. Choose "Custom layers" and select
senzor-apm-node. -
Update function configuration:
- Handler:
@senzops/apm-node/dist/lambda-handler.handler - Environment variables:
SENZOR_API_KEY= your API keySENZOR_LAMBDA_HANDLER= your original handler (e.g.,index.handler)NODE_OPTIONS=--require @senzops/apm-node/register
- Handler:
resource "aws_lambda_layer_version" "senzor_apm" {
filename = "senzor-apm-layer.zip"
layer_name = "senzor-apm-node"
compatible_runtimes = ["nodejs18.x", "nodejs20.x", "nodejs22.x"]
description = "Senzor APM Node.js Lambda Extension Layer"
}
resource "aws_lambda_function" "api" {
function_name = "my-function"
runtime = "nodejs20.x"
handler = "@senzops/apm-node/dist/lambda-handler.handler"
filename = "function.zip"
role = aws_iam_role.lambda.arn
layers = [aws_lambda_layer_version.senzor_apm.arn]
environment {
variables = {
SENZOR_API_KEY = var.senzor_api_key
SENZOR_LAMBDA_HANDLER = "index.handler"
NODE_OPTIONS = "--require @senzops/apm-node/register"
}
}
}When you prefer code-level control or cannot use Lambda Layers:
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
export const handler = Senzor.wrapLambda(async (event, context) => {
// Your Lambda logic
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
});Both the Extension Layer and code-level wrapper capture the same telemetry:
Cold Start Detection:
The first invocation in each container is tagged with faas.coldstart: true. Subsequent warm invocations are tagged false.
Trigger-Type Detection: The wrapper inspects the event shape and automatically detects:
| Trigger | Detection |
|---|---|
| API Gateway v1 (REST API) | event.httpMethod or event.requestContext.httpMethod |
| API Gateway v2 (HTTP API) | event.requestContext.http.method |
| Application Load Balancer | event.requestContext.elb |
| SQS | event.Records[0].eventSource === 'aws:sqs' |
| SNS | event.Records[0].EventSource === 'aws:sns' |
| DynamoDB Streams | event.Records[0].eventSource === 'aws:dynamodb' |
| S3 | event.Records[0].eventSource === 'aws:s3' |
| EventBridge | event.source + event.detail-type + event.detail |
| Scheduled (CloudWatch Events) | event.source === 'aws.events' |
For HTTP triggers (API Gateway, ALB), the wrapper extracts method, path, headers, client IP, and status code. For messaging triggers, it extracts queue/topic names, batch sizes, and table names.
Lambda Context Extraction:
| Attribute | Source |
|---|---|
faas.name |
context.functionName |
faas.version |
context.functionVersion |
faas.execution |
context.awsRequestId |
faas.max_memory |
context.memoryLimitInMB |
faas.coldstart |
Module-level boolean flag |
faas.trigger |
http, pubsub, datasource, timer, other |
cloud.provider |
aws |
cloud.platform |
aws_lambda |
cloud.region |
AWS_REGION env var |
cloud.account.id |
Parsed from invoked ARN |
cloud.resource_id |
context.invokedFunctionArn |
aws.log.group.names |
context.logGroupName |
Forced Flush:
After every invocation, the wrapper calls await Senzor.flush() before returning to the Lambda runtime. Lambda freezes the process immediately after the handler returns.
Lambda Extensions API:
The wrapper registers as an internal Lambda extension to receive SHUTDOWN lifecycle events. This provides a safety-net flush when the execution environment is being terminated.
When running inside Lambda (detected via AWS_LAMBDA_FUNCTION_NAME env var), the SDK automatically optimizes settings:
| Setting | Default | Lambda Override |
|---|---|---|
runtimeMetrics |
true |
false (meaningless per-invocation) |
batchSize |
100 |
10 (short-lived invocations) |
flushInterval |
10000 |
0 (flush on demand only) |
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: '<YOUR_APM_KEY>' });
export default {
fetch: Senzor.worker(async (request, env, ctx) => {
const url = new URL(request.url);
return new Response(JSON.stringify({ path: url.pathname }), {
headers: { 'content-type': 'application/json' },
});
}),
};The worker wrapper automatically:
- Starts a trace with method, path, headers, and client IP
- Captures response status code
- Uses
ctx.waitUntil()for non-blocking flush when available - Falls back to
await flush()otherwise
// server/plugins/senzor.ts
import { Senzor } from '@senzops/apm-node';
export default defineNitroPlugin((nitroApp) => {
Senzor.init({ apiKey: '<YOUR_APM_KEY>' });
Senzor.nitroPlugin(nitroApp);
});Complete table of all 43+ auto-instrumentations. All activate automatically when the library is present in node_modules and imported by your application.
| Instrumentation Key | npm Package(s) | Captured Signals |
|---|---|---|
http |
Node built-in http/https |
Inbound requests, outbound calls, status codes, timing |
fetch |
Global fetch |
Outbound HTTP, W3C Traceparent propagation |
undici |
undici |
Outbound HTTP via Node's native client |
express |
express |
Route matching, middleware chain, error capture |
fastify |
fastify |
Route matching, hooks, lifecycle spans |
koa |
koa |
Middleware stack, route detection |
nestjs |
@nestjs/core |
Controllers, Guards, Interceptors, Pipes |
hapi |
@hapi/hapi |
Route handling, request lifecycle |
restify |
restify |
Route matching, handler chain |
connect |
connect |
Middleware stack |
| Instrumentation Key | npm Package(s) | Captured Signals |
|---|---|---|
pg |
pg |
Queries, prepared statements, row counts, sanitized SQL |
mongo |
mongodb |
find, insert, update, delete, aggregate, bulkWrite, cursor |
mongoose |
mongoose |
Model operations, model/collection names |
mysql |
mysql, mysql2 |
Queries, sanitized SQL, row counts |
redis |
redis, ioredis |
All commands (GET, SET, HGETALL, DEL, etc.) |
knex |
knex |
Query builder, raw queries, transactions, streaming |
tedious |
tedious |
T-SQL queries, stored procedures, row counts, batch SQL |
cassandra |
cassandra-driver |
CQL queries, batch operations, prepared statements, consistency |
memcached |
memcached |
get, set, gets, cas, append, prepend, incr, decr, del, flush |
| Instrumentation Key | npm Package(s) | Captured Signals |
|---|---|---|
kafka |
kafkajs |
Producer send, consumer processing, topic, partition, offset |
amqplib |
amqplib |
Publish, consume, ack/nack, queue, exchange, routing key |
socketio |
socket.io |
Event emit/receive, namespace, room, acknowledgements |
bullmq |
bullmq |
Worker jobs as task runs, queue delay, retries, dead-letter |
cron |
node-cron |
Scheduled jobs as task runs, schedule expression |
| Instrumentation Key | npm Package(s) | Captured Signals |
|---|---|---|
openai |
openai |
Chat completions, embeddings, images, audio, assistants, token usage |
anthropic |
@anthropic-ai/sdk |
Messages, completions, model, input/output tokens, stop reason |
google-genai |
@google/generative-ai, @google-cloud/vertexai |
generateContent, chat, embeddings, countTokens, token usage |
azure-openai |
@azure/openai |
Chat, completions, embeddings, images, audio (v1.x) |
cohere |
cohere-ai |
Chat, generate, embed, rerank, classify, summarize, tokenize |
mistral |
@mistralai/mistralai |
Chat, FIM, embeddings, model, token usage |
aws-sdk (Bedrock) |
@aws-sdk/client-bedrock-runtime |
InvokeModel, Converse, token usage, model ID, finish reason |
| Instrumentation Key | npm Package(s) | Captured Signals |
|---|---|---|
aws-sdk |
@aws-sdk/*, @smithy/smithy-client |
All AWS service calls: S3, DynamoDB, SQS, SNS, Lambda, SES, etc. |
firebase |
firebase-admin, @google-cloud/firestore |
Firestore CRUD/queries, Auth (16 methods), FCM Messaging (9 methods) |
generic-pool |
generic-pool |
Pool acquire/release, pool size, pending count, queue size |
| Instrumentation Key | npm Package(s) | Captured Signals |
|---|---|---|
pino |
pino |
Trace/span ID injection into log records |
winston |
winston |
Trace/span ID injection into transport output |
bunyan |
bunyan |
Trace/span ID injection into serialized records |
| Instrumentation Key | npm Package(s) | Captured Signals |
|---|---|---|
grpc |
@grpc/grpc-js |
Unary/streaming calls, service/method, status codes, metadata |
graphql |
graphql |
Resolver execution, operation name/type, field paths, errors |
dns |
Node built-in dns |
Lookups, resolve, hostname, record types, timing |
net |
Node built-in net |
TCP socket connect, data transfer, connection duration |
| Instrumentation Key | npm Package(s) | Captured Signals |
|---|---|---|
dataloader |
dataloader |
Batch load calls, batch size, individual key loads |
lru-memoizer |
lru-memoizer |
Memoized function calls, cache hit/miss |
fs |
Node built-in fs |
readFile, writeFile, stat, readdir, mkdir, unlink, rename, etc. |
All AI SDK instrumentations follow OpenTelemetry GenAI semantic conventions.
| Attribute | Description |
|---|---|
gen_ai.system |
openai, anthropic, google_ai, vertex_ai, azure_openai, cohere, mistral, aws_bedrock |
gen_ai.request.model |
Model name from request (e.g., gpt-4o, claude-sonnet-4-20250514, gemini-1.5-pro) |
gen_ai.response.model |
Actual model from response (may differ for aliases) |
gen_ai.operation.name |
chat, embeddings, images, audio, fim, rerank, classify |
gen_ai.usage.input_tokens |
Prompt/input token count |
gen_ai.usage.output_tokens |
Completion/output token count |
gen_ai.usage.total_tokens |
Total tokens (when available) |
gen_ai.response.finish_reason |
stop, length, tool_calls, end_turn, etc. |
import OpenAI from 'openai';
const openai = new OpenAI();
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Hello' }],
});
// Span: "OpenAI chat gpt-4o" with token usageimport Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const message = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello' }],
});
// Span: "Anthropic messages claude-sonnet-4-20250514" with input/output tokensimport { GoogleGenerativeAI } from '@google/generative-ai';
const genai = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
const model = genai.getGenerativeModel({ model: 'gemini-1.5-pro' });
const result = await model.generateContent('Hello');
// Span: "Gemini generateContent gemini-1.5-pro" with token usageimport { CohereClient } from 'cohere-ai';
const cohere = new CohereClient({ token: process.env.COHERE_API_KEY! });
const response = await cohere.chat({ model: 'command-r-plus', message: 'Hello' });
// Span: "Cohere chat command-r-plus" with billed unitsimport { Mistral } from '@mistralai/mistralai';
const mistral = new Mistral({ apiKey: process.env.MISTRAL_API_KEY! });
const response = await mistral.chat.complete({
model: 'mistral-large-latest',
messages: [{ role: 'user', content: 'Hello' }],
});
// Span: "Mistral chat mistral-large-latest" with token usageAWS Bedrock calls made through @aws-sdk/client-bedrock-runtime are automatically captured with GenAI attributes via the AWS SDK instrumentation. Both InvokeModel and Converse APIs are supported.
import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime';
const client = new BedrockRuntimeClient({ region: 'us-east-1' });
const response = await client.send(new InvokeModelCommand({
modelId: 'anthropic.claude-3-sonnet-20240229-v1:0',
body: JSON.stringify({ messages: [{ role: 'user', content: 'Hello' }] }),
}));
// Span: "AWS BedrockRuntime InvokeModel" with gen_ai.* attributes and token usageimport { OpenAIClient, AzureKeyCredential } from '@azure/openai';
const client = new OpenAIClient(endpoint, new AzureKeyCredential(key));
const response = await client.getChatCompletions('gpt-4-deployment', messages);
// Span: "Azure OpenAI chat gpt-4-deployment" with token usageNote: Azure OpenAI SDK v2+ wraps the standard openai npm package internally, which is already covered by the OpenAI instrumentation.
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);Span: Postgres SELECT with db.system.name: 'postgresql', db.operation.name: 'SELECT', sanitized SQL, row count.
const user = await db.collection('users').findOne({ _id: userId });Span: Mongo users findOne with db.system.name: 'mongodb', db.collection.name: 'users', db.operation.name: 'findOne'.
const user = await User.findById(userId).exec();Span includes model name, collection name, and operation.
const [rows] = await connection.execute('SELECT * FROM users WHERE id = ?', [userId]);Span: MySQL SELECT with sanitized SQL and row count.
await redis.get(`user:${userId}`);Span: Redis GET with command name and key.
const users = await knex('users').where({ active: true }).select('*');Span: Knex SELECT users with query builder context, sanitized SQL, and binding count.
const request = new Request('SELECT * FROM Users WHERE Id = @id', callback);
request.addParameter('id', TYPES.Int, userId);
connection.execSql(request);Span: SQLServer SELECT with sanitized T-SQL, row count, and stored procedure name.
const result = await client.execute('SELECT * FROM users WHERE id = ?', [userId], { prepare: true });Span: Cassandra SELECT with db.system.name: 'cassandra', keyspace, consistency level, sanitized CQL.
memcached.get('session:abc', (err, data) => { });Span: Memcached GET with command name and key.
// Producer
await producer.send({ topic: 'orders', messages: [{ value: JSON.stringify(order) }] });
// Span: "Kafka SEND orders" with topic, partition, message count
// Consumer
await consumer.run({
eachMessage: async ({ topic, message }) => { /* ... */ },
});
// Span: "Kafka RECEIVE orders" with topic, partition, offset// Publish
channel.publish('exchange', 'routing.key', Buffer.from(JSON.stringify(data)));
// Span: "RabbitMQ PUBLISH exchange" with exchange, routing key
// Consume
channel.consume('queue-name', (msg) => { channel.ack(msg); });
// Span: "RabbitMQ RECEIVE queue-name" with queue name, delivery tagio.on('connection', (socket) => {
socket.on('message', (data) => { /* ... */ });
socket.emit('response', { ok: true });
});
// Spans for emit and receive with event names, namespace, roomAll AWS services are instrumented through the single Client.send() dispatch point:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
await s3.send(new PutObjectCommand({ Bucket: 'my-bucket', Key: 'file.txt', Body: data }));
// Span: "AWS S3 PutObject" with rpc.service, rpc.method, aws.request_id, aws.regionCovers: S3, DynamoDB, SQS, SNS, Lambda, SES, CloudWatch, Kinesis, EventBridge, Secrets Manager, SSM, STS, IAM, EC2, ECS, EKS, RDS, ElastiCache, Redshift, Cognito, Route53, CloudFront, API Gateway, Step Functions, CodeBuild, KMS, Bedrock Runtime, and any other AWS service using SDK v3.
Firestore:
const doc = await db.collection('users').doc('user123').get();
await db.collection('users').doc('user123').set({ name: 'Alice' });
const snapshot = await db.collection('users').where('active', '==', true).get();
// Spans: "Firestore GET users/user123", "Firestore SET users/user123", "Firestore QUERY users"Captured operations: GET, SET, ADD, UPDATE, DELETE, CREATE, LIST, QUERY, TX_GET, BATCH_COMMIT.
Auth:
const user = await admin.auth().getUser(uid);
const token = await admin.auth().verifyIdToken(idToken);
// Spans: "Firebase Auth getUser", "Firebase Auth verifyIdToken"16 methods captured: createUser, getUser, getUserByEmail, getUserByPhoneNumber, listUsers, deleteUser, deleteUsers, updateUser, verifyIdToken, verifySessionCookie, createSessionCookie, revokeRefreshTokens, setCustomUserClaims, generateEmailVerificationLink, generatePasswordResetLink, generateSignInWithEmailLink.
FCM Messaging:
await admin.messaging().send({ notification: { title: 'Hi' }, token: deviceToken });
// Span: "Firebase FCM send" with success/failure counts9 methods captured: send, sendEach, sendEachForMulticast, sendMulticast, sendToDevice, sendToTopic, sendToCondition, subscribeToTopic, unsubscribeFromTopic.
// Server and client calls automatically instrumented
// Span: "grpc /package.Service/Method" with rpc.system, rpc.service, rpc.method, status codeSupports unary, client streaming, server streaming, and bidirectional streaming.
// Resolver-level instrumentation
// Span: "GraphQL query GetUser" with operation name, operation type, field pathCaptures query/mutation/subscription operations and individual field resolver execution.
import dns from 'dns';
dns.lookup('example.com', (err, address) => { /* ... */ });
// Span: "DNS lookup example.com" with hostname, record typeimport net from 'net';
const socket = net.createConnection({ host: 'db.internal', port: 5432 });
// Span: "TCP CONNECT db.internal:5432" with host, port, connection durationAll console.log, console.info, console.warn, console.error, and console.debug calls are automatically captured and correlated with the active trace or task context.
app.get('/users/:id', async (req, res) => {
console.info('fetching user', { userId: req.params.id });
// Log is captured with traceId, level "info", and attributes { userId: "123" }
const user = await getUser(req.params.id);
res.json(user);
});Pino, Winston, and Bunyan get trace/span IDs automatically injected into log records:
import pino from 'pino';
const logger = pino();
app.get('/users/:id', (req, res) => {
logger.info({ userId: req.params.id }, 'fetching user');
// Output includes: traceId, spanId alongside your log fields
});Disable console log capture:
Senzor.init({ apiKey: '...', autoLogs: false });BullMQ workers are captured as task runs with queue delay, retry counts, dead-letter detection, and CPU/memory resource metrics.
node-cron scheduled jobs are captured as task runs with the cron schedule expression.
const processPayment = Senzor.wrapTask(
'process_payment', // task name
'custom', // type: 'cron' | 'queue' | 'pipeline' | 'custom'
{ metadata: { owner: 'billing' } },
async (invoiceId: string) => {
await chargeCustomer(invoiceId);
}
);
await processPayment('inv_123');Senzor.startTask('rebuild_index', 'custom', { metadata: { trigger: 'manual' } }, async () => {
try {
await rebuildSearchIndex();
// endTask is called automatically on success
} catch (error) {
Senzor.captureException(error);
throw error; // endTask('failed') on throw
}
});Task payloads include:
taskName,taskType,status,durationqueueDelay,attempts,isDeadLetter(for BullMQ)resourceMetrics: heap memory delta, CPU user/system timetriggerTraceId: links tasks triggered by HTTP requests
const span = Senzor.startSpan('calculate_invoice', 'function');
try {
const total = await calculateInvoice(invoiceId);
span.end({ invoiceId, total }, 200);
} catch (error) {
span.end({ invoiceId, error: String(error) }, 500);
throw error;
}Span types: http, db, function, custom, rpc, messaging, dns, net.
For environments where framework wrappers can't be used:
Senzor.track({
method: 'POST',
route: '/webhooks/payment',
path: '/webhooks/payment',
status: 202,
duration: 18.7,
spans: [],
});The SDK captures these global events:
| Event | Severity |
|---|---|
uncaughtExceptionMonitor |
fatal |
uncaughtException |
fatal |
unhandledRejection |
error |
warning |
warning |
multipleResolves |
warning |
SIGTERM |
info |
SIGINT |
info |
Each captured error includes: stack trace, process context (pid, platform, uptime, NODE_ENV), memory snapshot (RSS, heap, external), and SDK metadata.
try {
await chargeCustomer(customerId);
} catch (error) {
Senzor.captureException(error, { customerId, operation: 'charge' });
throw error;
}Errors are correlated with the active trace or task context.
Collected every 15 seconds (configurable) and sent with trace batches.
| Metric | Description |
|---|---|
eventLoop.lag.p50 |
Event loop lag, 50th percentile (ms) |
eventLoop.lag.p99 |
Event loop lag, 99th percentile (ms) |
eventLoop.lag.max |
Event loop lag, maximum observed (ms) |
eventLoop.utilization |
Event loop utilization (0-1, from perf_hooks ELU) |
gc.duration.minor |
GC pause time, minor collections (ms) |
gc.duration.major |
GC pause time, major collections (ms) |
gc.duration.incremental |
GC pause time, incremental marking (ms) |
memory.heapUsed |
V8 heap used (bytes) |
memory.heapTotal |
V8 heap total (bytes) |
memory.rss |
Resident set size (bytes) |
memory.external |
External memory (bytes) |
memory.arrayBuffers |
ArrayBuffer memory (bytes) |
activeHandles |
Open handles (file descriptors, sockets) |
activeRequests |
Pending async requests |
Disable runtime metrics:
Senzor.init({ apiKey: '...', runtimeMetrics: false });Adjust collection interval:
Senzor.init({ apiKey: '...', runtimeMetricsInterval: 30000 }); // 30 secondsRuntime metrics are automatically disabled in AWS Lambda environments.
The SDK injects these headers on all outgoing HTTP calls:
traceparent: 00-{traceId}-{spanId}-01
x-senzor-trace-id: {traceId}
x-senzor-parent-span-id: {spanId}
Incoming requests with traceparent or x-senzor-trace-id headers are linked as child traces, enabling cross-service distributed trace visualization.
The SDK uses AsyncLocalStorage to maintain trace context across async boundaries. All child spans, logs, and errors within a request are automatically correlated to the correct trace without any manual threading.
Senzor.init({
apiKey: 'sz_apm_xxx', // Required
endpoint: 'https://api.senzor.dev', // Ingest endpoint
batchSize: 100, // Flush threshold
flushInterval: 10000, // Flush interval (ms)
flushTimeoutMs: 5000, // Per-request timeout (ms)
maxQueueSize: 10000, // Max queued items before drop
maxSpansPerTrace: 500, // Max spans per trace
maxAttributeLength: 2048, // Max string length
maxAttributes: 64, // Max attributes per object
captureHeaders: false, // Capture sanitized headers
captureDbStatement: true, // Capture sanitized SQL
instrumentations: true, // true | false | string[]
frameworkSpans: true, // Framework middleware/handler spans
captureMiddlewareSpans: true, // Middleware execution spans
captureRouterSpans: true, // Router dispatch spans
captureLifecycleHookSpans: true, // Lifecycle hook spans
autoLogs: true, // Console log capture
runtimeMetrics: true, // Runtime metrics collection
runtimeMetricsInterval: 15000, // Collection interval (ms)
debug: false, // SDK diagnostics
});| Option | Production Guidance |
|---|---|
apiKey |
Always use environment variables or secret manager. Never hardcode. |
batchSize |
Start at 100. Increase for high-throughput services. |
flushInterval |
10000 ms for servers. 0 for Lambda (flush on demand). |
maxQueueSize |
Keep bounded. Increase only after checking memory pressure. |
maxSpansPerTrace |
Keep 500 unless traces fan out heavily (e.g., batch operations). |
captureHeaders |
Keep false unless needed for debugging. |
debug |
Never enable in production unless actively troubleshooting. |
// Enable only specific instrumentations
Senzor.init({
apiKey: '...',
instrumentations: ['http', 'fetch', 'pg', 'redis', 'openai'],
});
// Disable all auto-instrumentation (manual APIs only)
Senzor.init({
apiKey: '...',
instrumentations: false,
});The preload entrypoint (@senzops/apm-node/register) reads:
| Variable | Maps To | Default |
|---|---|---|
SENZOR_API_KEY |
apiKey |
— |
SENZOR_APM_API_KEY |
apiKey (alt) |
— |
SENZOR_SERVICE_API_KEY |
apiKey (alt) |
— |
SENZOR_ENDPOINT |
endpoint |
https://api.senzor.dev |
SENZOR_APM_ENDPOINT |
endpoint (alt) |
— |
SENZOR_DEBUG |
debug |
false |
SENZOR_AUTO_LOGS |
autoLogs |
true |
SENZOR_BATCH_SIZE |
batchSize |
100 (or 10 in Lambda) |
SENZOR_FLUSH_INTERVAL |
flushInterval |
10000 (or 0 in Lambda) |
SENZOR_FLUSH_TIMEOUT_MS |
flushTimeoutMs |
5000 |
SENZOR_MAX_QUEUE_SIZE |
maxQueueSize |
10000 |
SENZOR_MAX_SPANS_PER_TRACE |
maxSpansPerTrace |
500 |
SENZOR_CAPTURE_HEADERS |
captureHeaders |
false |
SENZOR_CAPTURE_DB_STATEMENT |
captureDbStatement |
true |
SENZOR_FRAMEWORK_SPANS |
frameworkSpans |
true |
SENZOR_CAPTURE_MIDDLEWARE_SPANS |
captureMiddlewareSpans |
true |
SENZOR_CAPTURE_ROUTER_SPANS |
captureRouterSpans |
true |
SENZOR_CAPTURE_LIFECYCLE_HOOK_SPANS |
captureLifecycleHookSpans |
true |
SENZOR_RUNTIME_METRICS |
runtimeMetrics |
true (auto false in Lambda) |
SENZOR_RUNTIME_METRICS_INTERVAL |
runtimeMetricsInterval |
15000 |
Environment variable values always take precedence over programmatic configuration.
These keys are automatically redacted from attributes, headers, logs, and error context:
authorization, cookie, set-cookie, password, passwd, pwd, secret, token, api-key, x-api-key, access-token, refresh-token, client-secret, private-key
- URL routes are normalized:
/users/123becomes/users/:id - UUIDs and MongoDB ObjectIDs are replaced with
:idin route names - SQL statements are normalized to remove literal values
- Attribute count and string length are bounded
- Span count per trace is bounded
- Queue size is bounded with oldest-first eviction
- Keep
captureHeadersdisabled unless required for debugging - Never capture request/response bodies
- Use route templates (e.g.,
/users/:id) rather than actual paths in span names - Store IDs in metadata, not in operation names
- Use
instrumentations: [...]to disable instrumentations you don't need
The SDK batches telemetry and sends it asynchronously using two independent queues:
- APM Queue: traces, errors, logs, runtime metrics ->
/api/ingest/apm - Task Queue: task runs, task errors, task logs ->
/api/ingest/task
Behavior:
- Queues are bounded by
maxQueueSize(oldest items evicted first) - Flush triggers on
batchSizethreshold orflushIntervaltimer - Each ingest request has a
flushTimeoutMstimeout - Failed batches are requeued within queue limits (retry on next flush)
- SDK ingest requests are marked as internal (not traced recursively)
- A best-effort flush runs on
process.beforeExit - In Lambda, flush is forced before handler return + Extensions API SHUTDOWN hook
Always call await Senzor.flush() before the function exits in serverless environments (unless using wrapLambda or Senzor.worker(), which handle this automatically).
{
"traces": [{
"traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
"parentTraceId": "8cb1f2e2f43e4b2b8b3411c7df81e7e0",
"parentSpanId": "6c62c908a21d4f49",
"rootSpanId": "91f6c551d5a2403f",
"method": "GET",
"route": "/orders/:orderId",
"path": "/orders/ord_123",
"status": 200,
"duration": 53.29,
"ip": "203.0.113.10",
"userAgent": "Mozilla/5.0",
"timestamp": "2026-05-26T15:30:00.000Z",
"spans": [{
"spanId": "9d8a4d5f17e24d2a",
"parentSpanId": "91f6c551d5a2403f",
"name": "Postgres SELECT",
"type": "db",
"startTime": 5.41,
"duration": 12.45,
"status": 0,
"meta": {
"db.system.name": "postgresql",
"db.operation.name": "SELECT",
"db.query.text": "SELECT id FROM users WHERE id = $?"
}
}]
}],
"errors": [{
"errorClass": "Error",
"message": "Connection refused",
"stackTrace": "Error: Connection refused\n at ...",
"traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
"context": { "operation": "db_connect" },
"timestamp": "2026-05-26T15:30:00.000Z"
}],
"logs": [{
"message": "order processed",
"level": "info",
"traceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
"attributes": { "orderId": "ord_123" },
"timestamp": "2026-05-26T15:30:00.000Z"
}],
"runtimeMetrics": [{
"timestamp": "2026-05-26T15:30:00.000Z",
"eventLoop": { "lagP50": 1.2, "lagP99": 4.8, "lagMax": 12.1, "utilization": 0.34 },
"gc": { "minor": 2.1, "major": 0, "incremental": 0.8 },
"memory": { "heapUsed": 45000000, "heapTotal": 67000000, "rss": 89000000 }
}]
}{
"runs": [{
"runId": "3917bd35-b1d6-4e23-a1d2-d969e1a7d6a1",
"taskName": "billing:send_invoice_email",
"taskType": "queue",
"status": "success",
"duration": 188.3,
"queueDelay": 92,
"attempts": 1,
"triggerTraceId": "f3b2c2c9c70443f5a4b7f0ff6d5b9a17",
"metadata": { "jobId": "142", "queueName": "billing" },
"resourceMetrics": {
"memoryDeltaBytes": 1048576,
"cpuUserUs": 12000,
"cpuSystemUs": 3000
},
"isDeadLetter": false,
"spans": [],
"timestamp": "2026-05-26T15:30:00.000Z"
}],
"errors": [],
"logs": []
}FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist/ dist/
ENV SENZOR_API_KEY=sz_apm_xxx
CMD ["node", "-r", "@senzops/apm-node/register", "dist/server.js"]apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: api
image: my-api:latest
env:
- name: SENZOR_API_KEY
valueFrom:
secretKeyRef:
name: senzor-secrets
key: api-key
- name: NODE_OPTIONS
value: "-r @senzops/apm-node/register"{
"apps": [{
"name": "orders-api",
"script": "dist/server.js",
"node_args": "-r @senzops/apm-node/register",
"env": {
"SENZOR_API_KEY": "sz_apm_xxx",
"NODE_ENV": "production"
}
}]
}See Section 4: AWS Lambda Integration.
See Section 5: Cloudflare Workers Integration.
// api/route.ts
import Senzor from '@senzops/apm-node';
Senzor.init({ apiKey: process.env.SENZOR_API_KEY! });
export const GET = Senzor.wrapNextRoute(async (req) => {
const result = await fetchData();
return Response.json(result);
});- Check startup order. Preload mode must load before application modules:
node -r @senzops/apm-node/register server.js
- Programmatic mode: Initialize
Senzor.init()before importingpg,mongodb,redis, etc. - Check if the library is supported in the instrumentation table.
- Verify
SENZOR_API_KEYis set and valid. - Verify
endpointpoints to the correct Senzor environment. - Verify the runtime can reach the ingest endpoint (check firewall, VPC, DNS).
- Enable
debug: trueto see flush attempts. - In serverless environments, ensure
Senzor.flush()is called (or usewrapLambda/worker()).
The SDK deduplicates active traces. If duplicates still appear:
- Check whether both preload mode AND manual
track()calls are active for the same request. - Ensure
Senzor.requestHandler()is only attached once.
Senzor.init({
apiKey: '...',
maxQueueSize: 5000, // Reduce queue bounds
maxSpansPerTrace: 250, // Reduce span retention
maxAttributes: 32, // Reduce attribute count
maxAttributeLength: 1024, // Reduce string lengths
});Use framework wrappers that provide route templates:
app.use(Senzor.requestHandler()); // Captures /users/:id instead of /users/123For manual traces, use normalized routes:
Senzor.track({ method: 'GET', route: '/users/:id', path: '/users/123', ... });- Ensure
Senzor.init()or preload runs outside the handler (module scope). - Check that
NODE_OPTIONSis set correctly for Lambda Layer deployments. - Use
Senzor.wrapLambda()for proper cold start tagging.
// Initialization
Senzor.init(options: SenzorOptions) // Initialize SDK with API key
Senzor.preload(options?: Partial<SenzorOptions>) // Pre-install hooks (used by register.ts)
// Telemetry
Senzor.flush(): Promise<void> // Force flush all queued telemetry
Senzor.track(data: object) // Send a manual trace
Senzor.startSpan(name: string, type?: SpanType) // Start a manual child span
Senzor.captureException(error: unknown, ctx?: object) // Capture an error
// Task Monitoring
Senzor.wrapTask(name, type, options, fn) // Wrap function as monitored task
Senzor.startTask(name, type, options, fn) // Start task monitoring context
// Framework Wrappers
Senzor.requestHandler() // Express request middleware
Senzor.errorHandler() // Express error middleware
Senzor.fastifyPlugin // Fastify plugin
Senzor.wrapNextRoute(handler) // Next.js App Router wrapper
Senzor.wrapNextPages(handler) // Next.js Pages Router wrapper
Senzor.wrapH3(handler) // H3/Nuxt/Nitro event handler wrapper
Senzor.nitroPlugin // Nitro plugin for Cloudflare Workers
Senzor.worker(handler) // Cloudflare Workers fetch handler wrapper
Senzor.wrapLambda(handler) // AWS Lambda handler wrappernpm run buildOutput:
| File | Format | Description |
|---|---|---|
dist/index.js |
CommonJS | Main entry |
dist/index.mjs |
ESM | Main entry |
dist/index.global.js |
IIFE | Browser/CDN bundle |
dist/register.js |
CommonJS | Preload entry |
dist/register.mjs |
ESM | Preload entry |
dist/index.d.ts |
TypeScript | Type declarations |
dist/register.d.ts |
TypeScript | Type declarations |
Verify before publishing:
npx tsc --noEmit && npm run build