@swirepay-developer/common-logging-nodejs
v1.0.9
Published
Swirepay Common Logging SDK for Node.js
Downloads
802
Maintainers
Readme
Swirepay OpenTelemetry Node.js SDK
A drop-in observability library for Swirepay Node.js/TypeScript services. Call initObservability() once
and your service ships traces, metrics, and logs over OTLP — no agent, no extra scripts,
no per-environment config to copy around.
Features
- Agentless by default. Builds the OTel SDK in-process.
node app.jsis all you need. - All three signals — traces, metrics, logs — individually toggleable.
- Environment-aware. Resolves endpoint + sampling ratio from
local / staging / productionautomatically. - AppMetrics facade — Micrometer-style
count / recordTime / time / recordValue / gaugeover the OTel Meter API. @WithSpan/@Traced— wrap any method in a child span; caller awaits.@FireAndForget— wrap a method in a child span; caller is not blocked.@AutoTrace/@AutoTraceDeep— wrap every public method of a class (or its full prototype chain) in spans, no annotations required.TracedService/DetachedService— extend these base classes for zero-annotation tracing without decorators.traceProxy()— wrap any existing service instance with automatic per-method spans.continueTrace— stitch an upstream trace (Java, mobile, etc.) into the current service so the full flow appears as one trace in Tempo.@Scheduled/wrapScheduledFn— turn scheduled job executions into root spans.withCurrentContext— propagate OTel context into deferred async tasks / worker queues.withAsyncSpan— run async work as a child span under the current trace.- Kafka instrumentation —
wrapKafkaProducer/wrapKafkaConsumerHandlerfor end-to-end trace propagation. - DB instrumentation —
wrapPgPool/wrapMysqlPool/wrapMongoose/wrapMongoCollection/wrapDynamoDbClientfor CLIENT spans on every query. - Outbound HTTP — wraps global
fetchautomatically; provides an axios interceptor factory. - Winston + OTLP logging — every log line goes to console AND the OTel Logs SDK with automatic
trace_id/span_idinjection. - Structured domain logging —
logApiCall/logRequest/logWebhookwith automatic body sanitization and sensitive field redaction.
Quickstart
1. Install
npm install @swirepay-developer/common-logging-nodejs2. Start the local stack
cd java/deploy
docker compose up -d3. Initialize once at startup
// app.ts — must be the very first import
import { initObservability } from '@swirepay-developer/common-logging-nodejs'
const { logger, appMetrics, tracer } = await initObservability({
serviceName: pkg.name,
environment,
endpoint: {
protocol: 'http', // or 'grpc'
local: 'http://127.0.0.1:4318', // Use 127.0.0.1 to avoid IPv6 issues
},
resourceAttributes: {
'service.version': pkg.version,
},
logs: {
level: process.env.LOG_LEVEL,
mdcEnabled: true,
debugEnabled: process.env.DEBUG_ENABLED === 'true',
},
auth: {
type: 'bearer',
token: process.env.OTEL_AUTH_TOKEN ?? secret?.swirepayOpenTelemetry ?? '',
},
})
logger.info('Server started', { port: 3000 })4. Run the example service
npm run devConfiguration reference
All options are passed to initObservability().
| Option | Default | Description |
|-------------------------------------|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | true | Master switch. |
| mode | 'sdk' | 'sdk' (in-process) or 'agent' (OTel agent owns SDK). |
| serviceName | SERVICE_NAME env or 'unknown-service' | Logical service name. |
| serviceNamespace | — | Optional namespace (e.g. 'payments'). |
| environment | inferred from NODE_ENV | 'local' / 'staging' / 'production'. |
| endpoint.override | — | Override the env-derived OTLP endpoint. |
| endpoint.local | http://localhost:4318 | Default for local. |
| endpoint.staging | http://otel-collector.staging...:4318 | Default for staging. |
| endpoint.production | http://otel-collector.prod...:4318 | Default for production. |
| endpoint.protocol | 'http' | OTLP protocol: 'http' or 'grpc'. Can also be set via OTEL_EXPORTER_OTLP_PROTOCOL env var. |
| endpoint.override | — | Can be used to override existing endpoint for otel. |
| traces.enabled | true | Toggle traces. |
| traces.samplingRatio | 1.0 local / 0.3 staging / 0.1 production | Head-sampling ratio. |
| metrics.enabled | true | Toggle metrics. |
| metrics.exportIntervalMs | 30000 | OTLP push interval. |
| logs.enabled | true | Toggle OTLP log export. |
| logs.level | info | Logger Levels : error, warn, info, http, verbose, debug, silly |
| logs.mdcEnabled | true | Inject trace_id / span_id into log records. |
| logs.debugEnabled | true | Inject otel-dubugging logs into log records. |
| logs.maxBodySize | undefined (no limit) | Max chars for captured request/response bodies. Truncates with …[truncated] beyond this. |
| logs.additionalSensitiveKeys | [] | Extra field names to redact (case-insensitive substring match). Built-in list covers password, token, secret, apiKey, creditCard, cvv, ssn. |
| instrumentation.enabled | true | Master switch for all auto-instrumentation. |
| instrumentation.httpEnabled | true | HTTP server + client auto-instrumentation. |
| instrumentation.httpClientEnabled | true | Wrap global fetch with CLIENT spans. |
| resourceAttributes | {} | Extra resource attributes (e.g. { 'service.version': '1.0' }). |
| auth.type | — | bearer, basic, header |
| auth.headerName | — | If type is header then need to pass header name |
| auth.token | — | token for authentication |
Usage examples
Logger — structured domain methods
logApiCall, logRequest, and logWebhook produce structured log entries with automatic body sanitization and sensitive field redaction. All three automatically pick up the active trace_id / span_id.
const { logger } = await initObservability({ serviceName: 'my-service' })
// Outbound HTTP call
logger.logApiCall({
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
statusCode: 200,
duration: 142,
requestBody: { amount: 1000, currency: 'usd' },
responseBody: { id: 'ch_xxx', status: 'succeeded' },
})
// Inbound request (use alongside requestMiddleware or manually)
logger.logRequest({
method: 'POST',
path: '/api/orders',
statusCode: 201,
duration: 38,
userId: 'usr_123',
ip: '1.2.3.4',
requestBody: req.body,
responseBody: { orderId: 'ord_456' },
})
// Webhook receipt
logger.logWebhook({
source: 'stripe',
event: 'payment_intent.succeeded',
webhookId: 'evt_xxx',
status: 'processed',
processingTime: 55,
payload: event.data,
})Logger — requestMiddleware (Express)
Automatically calls logRequest() for every inbound request when the response finishes. Captures and sanitizes both request and response bodies.
import express from 'express'
const app = express()
app.use(express.json())
app.use(logger.requestMiddleware()) // register after body-parser, before routesThe middleware:
- Captures request body (from
req.bodyif body-parser ran, or from raw stream) - Captures response body (by wrapping
res.json()andres.send()) - Redacts sensitive fields (password, token, apiKey, creditCard, cvv, etc.)
- Reads
trace-id/span-idfrom response headers set byotelTraceContextMiddleware - Calls
logger.logRequest()onres.on('finish')with full context
getLogger — access the logger singleton anywhere
After initObservability() has been called, you can get the logger from any module without passing it around:
import { getLogger } from '@swirepay-developer/common-logging-nodejs'
const logger = getLogger()
logger.info('Processing payment', { orderId: 'ord_123' })getObservabilityContext — access the full context after init
import { getObservabilityContext } from '@swirepay-developer/common-logging-nodejs'
// Anywhere after initObservability() has been awaited:
const { logger, appMetrics, tracer } = getObservabilityContext()@AutoTrace class decorator
import { AutoTrace } from '@swirepay-developer/common-logging-nodejs'
@AutoTrace()
class PaymentService {
async charge(amount: number) { } // → INTERNAL span PaymentService.charge
async refund(id: string) { } // → INTERNAL span PaymentService.refund
// getters/setters/toString are skipped automatically
}@WithSpan decorator
import { WithSpan } from '@swirepay-developer/common-logging-nodejs'
class OrderService {
@WithSpan('placeOrder')
async place(order: Order): Promise<OrderResult> {
// logs emitted here carry trace_id/span_id
return await this.db.save(order)
}
}@Traced decorator
@Traced is an alias for @WithSpan that auto-names the span from the class and method name — no string argument needed.
import { Traced } from '@swirepay-developer/common-logging-nodejs'
class OrderService {
@Traced()
async place(order: Order): Promise<OrderResult> { // → span: "OrderService.place"
return await this.db.save(order)
}
}@FireAndForget decorator
Wraps an async method in a child span but does not block the caller. Use for side-effects like sending notifications or audit logging where the caller shouldn't wait.
import { FireAndForget } from '@swirepay-developer/common-logging-nodejs'
class NotificationService {
@FireAndForget()
async sendReceipt(payment: Payment): Promise<void> {
// caller is not blocked — runs as a child span under the same trace
await this.emailClient.send(payment.email, receipt)
}
}@AutoTraceDeep class decorator
Like @AutoTrace but instruments the full prototype chain on first instantiation. Use this on abstract base classes where subclasses inherit methods.
import { AutoTraceDeep } from '@swirepay-developer/common-logging-nodejs'
@AutoTraceDeep()
abstract class BaseRepository {
async findById(id: string) { } // → span on every concrete subclass
async save(entity: unknown) { }
}
class OrderRepository extends BaseRepository {
async findByCustomer(customerId: string) { } // also traced
}traceProxy — wrap any existing instance
Zero-annotation tracing for service instances you don't own or can't decorate. Every prototype method call gets a child span.
import { traceProxy } from '@swirepay-developer/common-logging-nodejs'
// Awaited — span ends when the method's promise resolves
const payments = traceProxy(new PaymentService(db))
// Fire-and-forget — caller is not blocked
const notify = traceProxy(new NotificationService(), { async: 'detach' })
// Exclude specific methods
const orders = traceProxy(new OrderService(), {
exclude: ['healthCheck', /^internal/],
})continueTrace — stitch an upstream trace
When another service (Java, mobile, etc.) passes its traceId + spanId to your Node.js service, use continueTrace to make the full end-to-end flow appear as a single trace in Tempo.
import { continueTrace, continueTraceSync } from '@swirepay-developer/common-logging-nodejs'
// Async handler — e.g. REST endpoint receiving trace context from a Java service
app.post('/api/orders', async (req, res) => {
const result = await continueTrace(
{
traceId: req.headers['x-trace-id'] as string,
spanId: req.headers['x-span-id'] as string,
},
async () => {
logger.info('Processing order') // trace_id matches the upstream Java service
return orderService.place(req.body)
}
)
res.json(result)
})
// Kafka consumer — traceId comes from message headers
await continueTrace(
{ traceId: message.headers['x-trace-id'], spanId: message.headers['x-span-id'] },
() => handleOrder(message.value)
)
// Synchronous variant
const result = continueTraceSync({ traceId, spanId }, () => processSync(data))withAsyncSpan — child span for concurrent async work
Runs async work as a new child span under the current trace — same traceId, new spanId. Use when the async work is causally related to the current request but runs after the response is sent.
import { withAsyncSpan } from '@swirepay-developer/common-logging-nodejs'
app.post('/payments', async (req, res) => {
const payment = await paymentService.charge(req.body)
res.status(201).json(payment)
// Runs after response — visible in Tempo under the same traceId
withAsyncSpan('notifications.sendReceipt', async () => {
await notificationService.sendReceipt(payment)
})
})Context propagation for async tasks
import { withCurrentContext } from '@swirepay-developer/common-logging-nodejs'
// Captures the current OTel context at submission time
const task = withCurrentContext(async () => {
await doWork() // runs under the original request's trace
})
setImmediate(task)TracedService class decorator
import { TracedService } from '@swirepay-developer/common-logging-nodejs'
class PaymentService extends TracedService {
async charge(amount: number) { } // → INTERNAL span PaymentService.charge
async refund(id: string) { } // → INTERNAL span PaymentService.refund
// getters/setters/toString are skipped automatically
}DetachedService class
import { DetachedService } from '@swirepay-developer/common-logging-nodejs'
class PaymentService extends DetachedService {
async charge(amount: number) { } // → INTERNAL span PaymentService.charge
async refund(id: string) { } // → INTERNAL span PaymentService.refund
// getters/setters/toString are skipped automatically
}otelTraceContextMiddleware for trace management
Continue an upstream trace or start a new one on each API request
Use this in SDK mode (the default). It works alongside OTel auto-instrumentation — it does not create its own span, it enriches the one the SDK already created.
import { otelTraceContextMiddleware } from '@swirepay-developer/common-logging-nodejs'
const app = express()
// Register before your routes — reads serviceName/environment from initObservability() automatically
app.use(otelTraceContextMiddleware())Override service name or environment if needed (rare):
app.use(otelTraceContextMiddleware({
serviceName: 'payments-service-v2',
environment: 'staging',
}))otelHttpServerMiddleware for trace management
Continue an upstream trace or start a new one on each API request (auto-instrumentation disabled)
Use this only when OTel auto-instrumentation is disabled — i.e. mode: 'agent' or instrumentation.httpEnabled: false. It creates its own SERVER span from scratch instead of enriching an existing one.
import { otelHttpServerMiddleware } from '@swirepay-developer/common-logging-nodejs'
const app = express()
// Register before your routes
app.use(otelHttpServerMiddleware())Axios interceptor
import axios from 'axios'
import { createAxiosInterceptor } from '@swirepay-developer/common-logging-nodejs'
const { request, response } = createAxiosInterceptor()
axios.interceptors.request.use(request)
axios.interceptors.response.use(response.onFulfilled, response.onRejected)Which one should I use?
- Default SDK mode →
otelTraceContextMiddleware(works alongside auto-instrumentation)- Agent mode or
instrumentation.httpEnabled: false→otelHttpServerMiddleware(creates its own span)
AppMetrics
const { appMetrics } = await initObservability({ serviceName: 'my-service' })
// Counter
appMetrics.count('orders.placed', { payment_method: 'card' })
appMetrics.countBy('orders.amount', 150.00)
// Timer
await appMetrics.time('orders.lookup.duration', async () => {
return await db.findOrder(id)
})
// Histogram
appMetrics.recordValue('orders.amount_cents', order.amountCents)
// Gauge
appMetrics.gauge('queue.depth', () => queue.size())
// Native OTel meter (async counters, exemplars, etc.)
const meter = appMetrics.otelMeter()
meter.createCounter('my.counter').add(1)@Scheduled decorator
import { Scheduled } from '@swirepay-developer/common-logging-nodejs'
class DirectoryPushJob {
@Scheduled()
async run() {
// Each execution becomes a root INTERNAL span
await this.pushPendingEntries()
}
}wrapScheduledFn (setInterval / node-cron)
import cron from 'node-cron'
import { wrapScheduledFn } from '@swirepay-developer/common-logging-nodejs'
cron.schedule('*/5 * * * *', wrapScheduledFn('CleanupJob.run', async () => {
await cleanup()
}))Kafka
import { Kafka } from 'kafkajs'
import { wrapKafkaProducer, wrapKafkaConsumerHandler } from '@swirepay-developer/common-logging-nodejs'
const kafka = new Kafka({ brokers: ['localhost:9092'] })
// Producer — send() emits PRODUCER spans + injects traceparent header
const producer = wrapKafkaProducer(kafka.producer())
await producer.connect()
await producer.send({ topic: 'orders', messages: [{ value: 'hello' }] })
// Consumer — eachMessage handler runs under a CONSUMER span
const consumer = kafka.consumer({ groupId: 'my-group' })
await consumer.run({
eachMessage: wrapKafkaConsumerHandler(async ({ topic, message }) => {
// upstream trace context is restored from message headers
logger.info('Processing message', { topic })
}),
})Database (pg)
import { Pool } from 'pg'
import { wrapPgPool } from '@swirepay-developer/common-logging-nodejs'
const pool = wrapPgPool(new Pool({ connectionString: process.env.DATABASE_URL }), 'mydb')
// Every pool.query() now produces a CLIENT span with db.statement attribute
const result = await pool.query('SELECT * FROM orders WHERE id = $1', [id])Database (MySQL2)
import mysql from 'mysql2/promise'
import { wrapMysqlPool } from '@swirepay-developer/common-logging-nodejs'
const pool = wrapMysqlPool(mysql.createPool({ ... }), 'mydb')
// Every pool.query() now produces a CLIENT span with db.statement attribute
const result = await pool.query('SELECT * FROM orders WHERE id = 1', [id])Database (mongoose)
import mongoose from 'mongoose'
import { wrapMongoose } from '@swirepay-developer/common-logging-nodejs'
await mongoose.connect(process.env.MONGO_URI!)
wrapMongoose(mongoose) // pass the mongoose instance
// DocumentDB (same driver, different URI):
await mongoose.connect('mongodb://user:[email protected]:27017/mydb?tls=true&...')
wrapMongoose(mongoose, 'mongodb') // 'mongodb' | 'documentdb' based on env set it upDatabase (native driver)
import { MongoClient } from 'mongodb'
import { wrapMongoCollection } from '@swirepay-developer/common-logging-nodejs'
const client = new MongoClient(process.env.MONGO_URI!)
await client.connect()
const raw = client.db('mydb').collection('orders')
const ordersA = wrapMongoCollection(raw) // MongoDB
// DocumentDB:
const ordersB = wrapMongoCollection(raw, 'documentdb')Database (DynamoDB)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
import { wrapDynamoDbClient } from '@swirepay-developer/common-logging-nodejs'
// Plain client
const dynamo = wrapDynamoDbClient(new DynamoDBClient({ region: 'us-east-1' }))
// Document client
const docClient = wrapDynamoDbClient(
DynamoDBDocumentClient.from(new DynamoDBClient({ region: 'us-east-1' }))
)What initObservability() does on startup
- Resolves deployment environment (explicit →
NODE_ENV→local) and computes effective OTLP endpoint + sampling ratio. - Builds
NodeSDKwithOTLPTraceExporter,OTLPMetricExporter,OTLPLogExporter, andgetNodeAutoInstrumentations(). - Starts the SDK (registers global
TracerProvider,MeterProvider,LoggerProvider). - Wraps global
fetchwith CLIENT span creation. - Creates a Winston logger wired to
OtlpWinstonTransport(console + OTLP). - Exposes
AppMetrics,MetricsService,Tracer,Meter,OtelLoggerbeans. - Registers
SIGTERM/SIGINThandlers for graceful SDK flush + shutdown.
Compatibility
| Component | Versions | |---|---| | Node.js | 18+ | | TypeScript | 5.x | | Express | 4.x | | OpenTelemetry SDK | 0.209.x / API 1.9.x | | kafkajs | 2.x | | pg | 8.x | | mysql2 | 3.x |
Production
- Set
NODE_ENV=production(orenvironment: 'production') to get 10% head sampling automatically. - Point
endpoint.productionat your OTel Collector service. - Use
traces.samplingRatioto override sampling per service. - Disable unused signals:
traces: { enabled: false }/metrics: { enabled: false }.
