unnbound-logger-sdk
v3.2.2
Published
A structured logging library with TypeScript support using Pino. Provides consistent, well-typed logging with automatic logId, workflowId, traceId, and deploymentId tracking across operational contexts.
Readme
Unnbound Logger
A structured logging library with TypeScript support built on Pino. Provides consistent, well-typed logging across different operational contexts with automatic trace and span tracking. All logs are output in JSON format for better machine readability and parsing.
Installation
npm install unnbound-logger-sdkBasic Usage
import { logger } from 'unnbound-logger-sdk';
// Log with string messages
logger.info('Application started');
logger.warn('Resource usage high');
logger.error({ err: new Error('Database connection failed') }, 'Something bad happened');
logger.debug('Debug information');
logger.trace('Trace information');
// Log with object messages (merged into top level)
logger.info({ event: 'user_login', userId: '123' }, 'Event received.');
// Results in: { "event": "user_login", "userId": "123", "message": "Event received.", ... }
// Log with both string message and metadata (metadata merged into top level)
logger.info({ userId: '123' }, 'User logged in');
// Results in: { "userId": "123", "message": "User logged in", ... }Log Format
All logs follow a standardized format based on Pino with additional context:
interface Log<T extends LogType = 'general'> {
logId: string; // Automatically generated unique ID for each log entry
level: LogLevel; // "info" | "debug" | "error" | "warn" | "trace"
message: string;
type: T; // "general" | "http" | "sftp"
traceId?: string; // Automatically included when in trace context
spanId?: string; // Automatically included when in span context
serviceId?: string; // From UNNBOUND_SERVICE_ID environment variable
deploymentId?: string; // From UNNBOUND_DEPLOYMENT_ID environment variable
workflowId?: string; // From UNNBOUND_WORKFLOW_ID environment variable
environment?: string; // From TEMPER_ENVIRONMENT, WORKFLOW_ENV, UNNBOUND_ENVIRONMENT, or ENVIRONMENT, whichever is set first
err?: unknown; // Only present for Error objects
duration?: number; // Duration in milliseconds for span operations
http?: T extends 'http' ? HttpPayload : never;
sftp?: T extends 'sftp' ? SftpPayload : never;
}Workflow and Deployment Tracking
Workflow Tracking
The logger includes a workflowId field in all log entries for tracking operations across services:
# Set the workflow ID in your environment
export UNNBOUND_WORKFLOW_ID="order-processing-12345"
export UNNBOUND_WORKFLOW_URL="https://workflows.example.com/order-processing-12345"
export UNNBOUND_SERVICE_ID="order-service"
# Or in your deployment configuration
UNNBOUND_WORKFLOW_ID=order-processing-12345
UNNBOUND_WORKFLOW_URL=https://workflows.example.com/order-processing-12345
UNNBOUND_SERVICE_ID=order-service// Import the logger - workflowId and serviceId are automatically set from environment
import { logger } from 'unnbound-logger-sdk';Deployment Tracking
The logger automatically includes a deploymentId field in all log entries. This field is populated from the UNNBOUND_DEPLOYMENT_ID environment variable, allowing you to track logs per deployment.
# Set the deployment ID in your environment
export UNNBOUND_DEPLOYMENT_ID="v1.2.3-prod-20231201"
# Or in your deployment configuration
UNNBOUND_DEPLOYMENT_ID=v1.2.3-prod-20231201If the environment variables are not set, the fields are omitted from the log entry. These fields help with:
- Workflow ID: Unique identifier for the workflow (logged in each entry)
- Workflow URL: Used internally for URL construction in webhook endpoints (not logged as a field)
- Service ID: Identifier for the specific service/component (logged in each entry)
- Deployment ID: Tracking logs across different application deployments (logged in each entry)
- Correlating issues: Link problems to specific workflows and releases
- Monitoring: Track health and performance across workflows and deployments
Object Logging Behavior
The logger follows Pino's argument order. The context object comes first and the message comes second.
The results below show only the fields each call contributes. The leading ... stands for the
fields the logger adds itself: level, logId, and source on every entry, plus workflowId,
serviceId, deploymentId, and environment when their environment variables are set, and
traceId and spanId when the call runs inside a trace or span. See Log Format.
Context Object with a Message
logger.info({ userId: '123', action: 'login' }, 'User action completed');
// Result: { ..., "userId": "123", "action": "login", "message": "User action completed" }Every property of the object is merged into the top level of the log entry.
A context object always needs a message alongside it. ILogger declares each level as
(object, message) or (message) and nothing else, so logger.info({ userId: '123' }) does not
compile.
Reversing the Order Drops Your Context
Passing the message first discards the object:
logger.info('User action completed', { userId: '123', action: 'login' });
// Result: { ..., "message": "User action completed" }
// userId and action are gone. No error, no warning.TypeScript rejects the reversed order, because info is declared only as (object, message) or
(message). Running tsc reports Argument of type 'string' is not assignable to parameter of
type 'object', so a build catches the mistake. Transpile-only runners such as tsx do not
type-check, so under those the fields disappear at runtime with no error. Always put the object
first.
Error Objects
error takes an object containing err plus a message:
logger.error({ err: new Error('Connection refused') }, 'Failed to sync order.');
// Result: { ...,
// "err": { "type": "Error", "message": "Connection refused", "stack": "..." },
// "message": "Failed to sync order." }Errors are serialized by Pino's stdSerializers.err, which emits type, message, and stack.
Unlike the other levels, error has no single-argument overload, so the message is required.
Redaction Masks
registerRedactionMasks replaces each registered string with [Redacted] wherever it appears in a log line: messages, fields, errors, and child-logger bindings. @ontemper/resources registers the secrets it resolves; register other secret values yourself:
import { logger, registerRedactionMasks } from 'unnbound-logger-sdk';
registerRedactionMasks([apiKey]);
const partnerLog = logger.child({ partner: 'acme', auth: `Bearer ${apiKey}` });
partnerLog.info('Calling partner.');
// Result: { ..., "partner": "acme", "auth": "Bearer [Redacted]", "message": "Calling partner." }- Masks replace text in string values only. A property name that contains a secret is written unchanged.
- Masks apply to the finished line when it is written. A mask registered after
child()orsetBindings()still redacts those bindings, and the output of custom serializers and formatters is masked too. - A mask registered inside a request scope applies to the whole scope, and stays active for the rest of the process after the scope ends. The process keeps at most 4096 of these retained masks and drops the oldest first; masks held by an active scope are not capped.
- A value that code transforms before logging (for example, Base64-encodes) no longer matches its mask.
Treat masking as a safeguard, not a guarantee: do not log secrets.
HTTP Request/Response Logging
import { logger, traceMiddleware } from 'unnbound-logger-sdk';
import express from 'express';
const app = express();
// Apply trace middleware for automatic HTTP logging
app.use(traceMiddleware());
// Example route
app.post('/api/users', (req, res) => {
// Your route handler code here
res.status(201).json({ id: '123', status: 'created' });
});The trace middleware automatically captures:
- Request method, URL, body, and headers (filtered for security)
- Response status code, body, and headers (filtered for security)
- Request duration
- Trace ID and span ID for correlation
- Automatic span tracking for the entire request lifecycle
Full URL Logging for Webhook Endpoints
When webhook endpoints receive incoming requests, the logger automatically constructs and logs the full URL using a smart fallback strategy:
- Preferred: Uses
UNNBOUND_WORKFLOW_URL- If set, this becomes the base URL for all logged requests - Fallback: Constructs from request headers - Uses protocol, host, and forwarded headers from the incoming request
# Set your workflow URL to ensure full URLs in logs
export UNNBOUND_WORKFLOW_URL="https://api.yourservice.com"
# Example webhook endpoints will be logged as:
# POST https://api.yourservice.com/webhooks/stripe
# POST https://api.yourservice.com/webhooks/githubimport express from 'express';
import { traceMiddleware } from 'unnbound-logger-sdk';
const app = express();
// Apply trace middleware for automatic logging
app.use(traceMiddleware());
// Webhook endpoints - URLs automatically logged with full domain
app.post('/webhooks/stripe', (req, res) => {
// Request logged as: https://api.yourservice.com/webhooks/stripe
res.status(200).send('OK');
});
app.post('/webhooks/github', (req, res) => {
// Request logged as: https://api.yourservice.com/webhooks/github
res.status(200).send('OK');
});This ensures webhook logs contain the complete URL for easy debugging and monitoring.
SFTP Operations Logging
The logger supports structured logging for SFTP operations with automatic span tracking:
import { logger, startSpan } from 'unnbound-logger-sdk';
// Example SFTP operation with automatic logging
const uploadFile = async (filePath: string, content: string) => {
return await startSpan(
'SFTP upload operation',
async () => {
logger.info({ filePath, contentLength: content.length }, 'Uploading file');
return { success: true, filePath };
},
() => ({
type: 'sftp',
sftp: {
host: 'sftp.example.com',
operation: 'upload',
path: filePath,
content: content,
bytes: content.length,
},
})
);
};The SFTP logging automatically captures:
- Host information
- Operation type (connect, upload, download, list, delete, etc.)
- File paths and content
- Byte counts for transfers
- Operation duration
- Success/failure status
Middleware Usage
Express Trace Middleware
The library provides a comprehensive trace middleware for Express applications that automatically handles trace context and HTTP logging:
import { traceMiddleware } from 'unnbound-logger-sdk';
import express from 'express';
const app = express();
// Apply the comprehensive trace middleware globally
app.use(traceMiddleware());The trace middleware automatically:
- Generates and maintains trace IDs across the request lifecycle
- Logs incoming requests with method, URL, headers, and body
- Logs outgoing responses with status code, headers, body, and duration
- Measures request duration automatically
- Handles errors and logs them appropriately
- Captures response bodies for logging
- Creates spans for the entire request lifecycle
Axios Trace Middleware
For comprehensive logging of outgoing HTTP requests made with Axios:
import { traceAxios } from 'unnbound-logger-sdk';
import axios from 'axios';
// Create an axios instance and wrap it with tracing
const client = traceAxios(axios.create());
// All requests made with this client will be automatically logged
client.get('https://api.example.com/data');The Axios middleware:
- Logs outgoing requests with method, URL, headers, and body
- Maintains trace context across requests by propagating trace IDs
- Logs successful responses with status, headers, body, and duration
- Logs error responses with detailed error information
- Creates spans for each HTTP request
- Supports request/response filtering through configuration
Manual tracing
In case the function doesn't run inside an HTTP handler (for example a cron job), you can assign a trace id manually by using the withTrace function:
import { withTrace } from 'unnbound-logger-sdk';
const operation = async (value: number) => {
logger.info({ value }, 'Processing value');
return value * 2;
};
setInterval(() => withTrace(() => operation(13)), 1000);Function Tracing with startSpan
The startSpan function allows you to wrap any async operation with automatic span tracking and logging. This is particularly useful for maintaining consistent trace IDs across async operations and distributed systems:
import { logger, startSpan } from 'unnbound-logger-sdk';
const operation = async (value: number) => {
logger.info({ value }, 'Processing value');
return value * 2;
};
const result = await startSpan('Processing operation', () => operation(21), () => ({
operationType: 'calculation',
}));Note: In case the traceId is missing from the context, startSpan will generate one. It is recommended to use a single traceId across your handler though, so always consider using traceMiddleware and withTrace to inject the traceId instead of relying on startSpan to create one.
Async Operations
The span context is maintained across async operations:
const asyncOperation = async (value: number) => {
logger.info({ value }, 'First step');
await someAsyncWork();
logger.info({ value }, 'Second step');
return value * 2;
};
const result = await startSpan('Async operation', () => asyncOperation(21), () => ({
operationType: 'async_calculation',
}));Benefits
- Automatic span ID generation and tracking
- Consistent trace context across async operations
- Automatic duration measurement
- Error handling and logging
- Type-safe implementation
- Works with async functions
- Maintains separate span contexts for different operations
Note: When using the logger within spans, you don't need to manually manage trace IDs. The logger automatically includes the current trace ID and span ID in all log entries.
API Reference
logger
The main logger instance that provides all logging functionality using Pino.
Usage
import { logger } from 'unnbound-logger-sdk';The logger is a Pino instance with additional context automatically included from environment variables and trace context.
Methods
logger.trace(object: {}, message: string): voidlogger.trace(message: string): voidlogger.debug(object: {}, message: string): voidlogger.debug(message: string): voidlogger.info(object: {}, message: string): voidlogger.info(message: string): voidlogger.warn(object: {}, message: string): voidlogger.warn(message: string): voidlogger.error<O extends { err: unknown }>(object: O, message: string): void
traceMiddleware
Express middleware for automatic HTTP request/response logging and trace context management.
Usage
import { traceMiddleware } from 'unnbound-logger-sdk';
app.use(traceMiddleware(options?: HttpOptions));HttpOptions:
traceHeaderKey?: string- Custom trace header name (default: 'x-unnbound-trace-id')messageHeaderKey?: string- Custom message header name (default: 'x-message-id')ignoreTraceRoutes?: string[]- Routes to ignore in middleware (default: ['/health', '/healthcheck'])
traceAxios
Wraps an Axios instance with automatic request/response logging and trace context propagation.
Usage
import { traceAxios } from 'unnbound-logger-sdk';
import axios from 'axios';
const client = traceAxios(axios.create(), options?: HttpOptions);startSpan
Creates a span for tracking async operations with automatic logging and duration measurement.
Usage
import { startSpan } from 'unnbound-logger-sdk';
const result = await startSpan<T>(
spanName: string,
callback: () => Promise<T>,
getter?: LogPayloadGetter<T>
);Parameters:
spanName: The name of the span for loggingcallback: The async function to execute within the spangetter: Optional function to generate log payload based on operation result/error
getTraceId
Generates a new trace ID for manual trace context management.
Usage
import { getTraceId } from 'unnbound-logger-sdk';
const traceId = getTraceId();
console.log(traceId); // "550e8400-e29b-41d4-a716-446655440000"Environment Variables
UNNBOUND_WORKFLOW_ID- Workflow identifier (included in all logs)UNNBOUND_SERVICE_ID- Service identifier (included in all logs)UNNBOUND_DEPLOYMENT_ID- Deployment identifier (included in all logs)UNNBOUND_WORKFLOW_URL- Base URL for webhook endpoint loggingTEMPER_ENVIRONMENT,WORKFLOW_ENV,UNNBOUND_ENVIRONMENT,ENVIRONMENT- Environment name, read in that order (included in all logs). Temper injectsTEMPER_ENVIRONMENT; the other names are legacy fallbacks.LOG_LEVEL- Log level (default: 'debug')
