@cobre-npm/library-nodejs-telemetry
v1.5.1
Published
Nodejs Cobre telemetry library with OpenTelemetry
Keywords
Readme
Telemetry library Node.js
A specialized Node.js library for OpenTelemetry-based telemetry and observability. This library provides comprehensive tracing and logging capabilities using OpenTelemetry, designed to streamline observability across your Node.js applications.
Features
- Distributed Tracing - Complete OpenTelemetry tracing with AWS X-Ray, W3C Trace Context, and Baggage propagation
- Structured Logging - OpenTelemetry-based logging with automatic context propagation
- FaaS Metrics - Opt-in
faas.*golden signals for AWS Lambda handlers, matching the Python telemetry library - AWS Integration - Built-in support for AWS Lambda, AWS SDK, Express, and HTTP instrumentation
- OTLP Export - Native support for OTLP (OpenTelemetry Protocol) exporters for traces and logs
Installation
npm install @cobre-npm/library-nodejs-telemetry
# or
pnpm install @cobre-npm/library-nodejs-telemetryExamples
Tracing
Initialize OpenTelemetry tracing in your application to enable distributed tracing across your services.
import { TracingAdapter } from '@cobre-npm/library-nodejs-telemetry/dist'
import packageJson from './package.json'
const serviceName = 'my-service'
const serviceVersion = packageJson.version
// Initialize tracing with service configuration
TracingAdapter.init({ serviceName, serviceVersion })
// Graceful shutdown
process.on('SIGTERM', async () => {
await TracingAdapter.shutdown()
process.exit(0)
})Features:
- Automatic instrumentation for HTTP, Express, AWS Lambda, and AWS SDK
- AWS X-Ray propagation support
- W3C Trace Context and Baggage propagation
- OTLP HTTP exporter for traces
Supported Instrumentations:
@opentelemetry/instrumentation-http- HTTP requests@opentelemetry/instrumentation-express- Express.js framework@opentelemetry/instrumentation-aws-lambda- AWS Lambda functions@opentelemetry/instrumentation-aws-sdk- AWS SDK calls
FaaS Metrics
TracingAdapter.init wires an OTLP metric exporter, but instruments have to be recorded
explicitly. withFaasMetrics wraps an AWS Lambda handler and records the faas.* golden
signals the New Relic "Golden Signals - FaaS (Lambda)" dashboard queries by name.
Opt-in and additive: an unwrapped handler behaves exactly as before, and tracing is
untouched — AwsLambdaInstrumentation still owns the invocation span.
import { withFaasMetrics } from '@cobre-npm/library-nodejs-telemetry/dist'
const handler = async (event, context) => {
return { statusCode: 200, body: 'ok' }
}
// Apply outermost, on the function AWS invokes
exports.handler = withFaasMetrics(handler)Metrics recorded per invocation:
| Metric | Type | Unit | When |
| ------ | ---- | ---- | ---- |
| faas.invoke_duration | histogram | s | always |
| faas.invocations | counter | — | handler succeeded |
| faas.errors | counter | — | handler threw, or returned a 5xx statusCode |
| faas.coldstarts | counter | — | first invocation in the container |
Attributes on every data point:
faas.trigger (http, pubsub, datasource, timer or other), inferred from the event
shape. HTTP triggers — API Gateway v1 and v2, Function URLs and Application Load Balancer —
add http.request.method and http.route; SQS and SNS triggers add the messaging.*
attributes.
http.route is the route template (/v1/items/{id}), taken from resource on v1 or
routeKey on v2, and is omitted when the event carries no template. The resolved path is
never used: as a metric dimension it produces one time series per path parameter value. Put
the resolved path on the span instead, where cardinality is free.
Every invocation also adds http.response.status_code when the handler returned one, and
error.type when it failed. These land on faas.invoke_duration as well as on the
counters, so the histogram is partitioned by status code and error class — useful for
reading the latency of 5xx responses separately, at the cost of extra series on the
highest-volume instrument.
Metrics are force-flushed before the handler returns, since a Lambda container freezes immediately afterwards and the periodic reader would never export them. That flush is a request to the collector, so it adds to the invocation's billed duration.
Requirements:
TracingAdapter.initmust have run before the first invocation. Instruments are created lazily for this reason — creating them earlier would bind them to the API's no-op provider.- Set
COBRE_ENVon the function, ordeployment.environment.nameis reported asUNKNOWNand the dashboard's environment filter will not list the service.
Metric temporality needs no configuration on Lambda: TracingAdapter.init defaults
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE to delta when
AWS_LAMBDA_FUNCTION_NAME is present, because New Relic expects delta and cumulative
counters reset unpredictably as Lambda recycles containers. Setting the variable explicitly
still wins, and long-lived services outside Lambda keep the exporter's own default.
flushFaasMetrics() and getFaasInstruments() are also exported, for handlers that record
their own data points onto the same series outside the wrapper.
Logging
The logging utility provides structured logging capabilities with OpenTelemetry support. It follows RFC5424 severity levels:
emerg: 0 - Emergencyalert: 1 - Alertcrit: 2 - Criticalerror: 3 - Errorwarning: 4 - Warningnotice: 5 - Noticeinfo: 6 - Informationaldebug: 7 - Debug
Basic Usage
import { log } from '@cobre-npm/library-nodejs-telemetry/dist'
// Log different severity levels
log.info({ message: 'Application started', content: { port: 3000 } })
log.error({ message: 'Failed to connect', content: { error: 'Connection timeout' } })
log.warning({ message: 'Rate limit approaching', content: { remaining: 10 } })
log.debug({ message: 'Processing request', content: { requestId: '123' } })Initialize OpenTelemetry Logging
Initialize OpenTelemetry logging with your service name and version:
import { log } from '@cobre-npm/library-nodejs-telemetry/dist'
import packageJson from './package.json'
const serviceName = 'my-service'
const serviceVersion = packageJson.version
// Initialize OpenTelemetry logging
log.initOpentelemetry(serviceName, serviceVersion)
// Or use automatic initialization with default values
log.initializeLogger()
// Or specify custom service name and version
log.initializeLogger('my-service', '1.0.0')Express Middleware
Use the built-in Express middleware to automatically extract and store user context from JWT tokens:
import express from 'express'
import { log } from '@cobre-npm/library-nodejs-telemetry/dist'
const app = express()
// Add logging middleware before your routes
app.use(log.loggerMiddleware)
app.get('/api/users', (req, res) => {
// User context (email, userId, clientId) is automatically available in logs
log.info({ message: 'Fetching users' })
res.json({ users: [] })
})The middleware automatically extracts:
email- User email from JWT tokenname(userId) - User name/ID from JWT tokenclient_id- Client ID from request headers
All subsequent logs will include this context automatically.
Custom Log Context
loggerMiddleware decides what goes on a log: it reads a JWT and fixes email, userId and clientId. When your service has no such JWT, or needs other keys, give the library the context yourself. The library stores it and adds it to the tags of every log made while the request is being handled, on top of the default email, userId and clientId (empty without loggerMiddleware); you decide the keys and where they come from.
import express from 'express'
import { log } from '@cobre-npm/library-nodejs-telemetry/dist'
const app = express()
// Query values come from the caller and may be arrays: keep only strings
const queryString = (value) => (typeof value === 'string' ? value : undefined)
// Express: extract the keys you want from the request
app.use(log.contextMiddleware((req) => ({
clientId: queryString(req.query.client_id),
entityId: queryString(req.query.entity_id), // undefined values are left out
})))
// Route params only exist once a route matches, so read them in a route-level middleware
app.get('/v1/refund/:refundId', log.contextMiddleware((req) => ({ refundId: req.params.refundId })), (req, res) => {
log.info({ message: 'Reading the refund' }) // tags: { clientId, entityId, refundId, ... }
res.json({})
})log.runWithContext(context, fn) is the same without Express, e.g. for a Lambda handler or a queue consumer:
export const handler = (event) =>
log.runWithContext({ clientId: event.clientId }, () => process(event))- The context travels with the async flow, so it is still there after an
await. - A nested
runWithContextmerges with the enclosing one, and theattrsof a single log win over the context. - A key whose value is
undefinedis left out. - The message gets no prefix, unlike
loggerMiddleware. - If the function given to
contextMiddlewarethrows, the error is logged and the request goes on without context. - It can be combined with
loggerMiddleware: the context wins on a repeated key in thetags, while the message prefix still comes from the JWT. - Never put a token, a password or other sensitive data in the context: it is added to every log and the library cannot tell what is sensitive. Values taken from the request are caller input: validate them before adding them.
The LogContext type is exported for typed helpers.
Logging with Attributes
Add custom attributes to your logs:
import { log } from '@cobre-npm/library-nodejs-telemetry/dist'
log.info({
message: 'User action performed',
content: { action: 'purchase', amount: 100 },
attrs: {
userId: 'user123',
sessionId: 'session456',
feature: 'checkout'
}
})Complete Setup Example
Here's a complete example of setting up both tracing and logging in an Express application:
import express from 'express'
import dotenv from 'dotenv'
import { TracingAdapter, log } from '@cobre-npm/library-nodejs-telemetry/dist'
import packageJson from './package.json'
// Load environment variables
dotenv.config()
const app = express()
const serviceName = 'my-service'
const serviceVersion = packageJson.version
// Initialize OpenTelemetry Tracing
TracingAdapter.init({ serviceName, serviceVersion })
// Initialize OpenTelemetry Logging
log.initOpentelemetry(serviceName, serviceVersion)
// Or use automatic initialization
log.initializeLogger(serviceName, serviceVersion)
// Add logging middleware
app.use(log.loggerMiddleware)
// Your routes
app.get('/health', (req, res) => {
log.info({ message: 'Health check requested' })
res.json({ status: 'ok' })
})
// Graceful shutdown
process.on('SIGTERM', async () => {
log.info({ message: 'Shutting down gracefully' })
await TracingAdapter.shutdown()
process.exit(0)
})
app.listen(3000, () => {
log.info({ message: 'Server started', content: { port: 3000 } })
})logMaskedObject
Utility function to mask sensitive data in logs while keeping a portion of the original value for identification purposes.
import { logMaskedObject } from '@cobre-npm/library-nodejs-telemetry/dist'
logMaskedObject(
{
username: 'DanielHernandez',
email: '[email protected]',
creditCard: '1234567890123456',
phone: '+1234567890'
},
5 // Keep last 5 characters
)Output (logged as info):
{
"username": "*****-*****-*****-*****andez",
"email": "*****-*****-*****-*****@example.com",
"creditCard": "*****-*****-*****-3456",
"phone": "*****-*****-*****-67890"
}Environment Variables
This library uses OpenTelemetry for all telemetry operations. None of the variables below are required — the library falls back to a default service configuration when they are absent.
| Variable | Read by | Effect |
| -------- | ------- | ------ |
| COBRE_ENV | tracing, logging | Value of the deployment.environment.name resource attribute. Defaults to UNKNOWN. |
| OTEL_EXPORTER_OTLP_ENDPOINT | OTLP exporters | Collector to export traces, logs and metrics to. |
| OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE | metric exporter | Defaults to delta on AWS Lambda, set by TracingAdapter.init. See FaaS Metrics. |
| OTEL_SEMCONV_STABILITY_OPT_IN | HTTP instrumentation | Defaults to http when unset. |
| NODE_ENV | logging | When it contains local, logs are also mirrored to the console. |
Architecture
Tracing Module
src/tracing/
├── adapter/
│ ├── tracing.adapter.ts # Main tracing adapter
│ └── config.ts # Service configuration
├── domain/
│ └── port/
│ └── out/
│ └── tracing.port # Port interface
└── entrypoints/
└── index.ts # Public exportsMetrics Module
src/metrics/
├── adapter/
│ ├── faasMeters.ts # faas.* instruments and force flush
│ └── faasMetrics.adapter.ts # withFaasMetrics handler wrapper
├── utils/
│ └── faasAttributes.utils.ts # Trigger classification and attribute keys
└── entrypoints/
└── index.ts # Public exportsLogging Module
src/logging/
├── adapter/
│ └── logging.adapter.ts # Main logging adapter
├── domain/
│ ├── interfaces/
│ │ └── tokenDecoded.interface.ts # Token interface
│ ├── logContext.ts # Context the consumer adds to every log
│ └── loggerProps.ts # Logger properties
└── entrypoints/
└── index.ts # Public exportsDependencies
OpenTelemetry Packages
@opentelemetry/api- Core OpenTelemetry API@opentelemetry/api-logs- Logging API@opentelemetry/sdk-node- Node.js SDK@opentelemetry/sdk-logs- Logging SDK@opentelemetry/exporter-trace-otlp-http- OTLP HTTP trace exporter@opentelemetry/exporter-logs-otlp-proto- OTLP Proto log exporter@opentelemetry/instrumentation-*- Various instrumentations@opentelemetry/propagator-aws-xray-lambda- AWS X-Ray propagation@opentelemetry/resources- Resource detection@opentelemetry/semantic-conventions- Semantic conventions
Other Dependencies
jwt-decode- JWT token decoding
Build
npm run buildThis will create the compiled output in the dist/ directory.
Test
npm testRuns jest with coverage. Only the logging module is measured so far (95% threshold); tracing and metrics have no tests yet.
License
ISC
