@uengage.io/js-logger
v0.10.0
Published
Structured observability/logging for uEngage platform services
Downloads
1,279
Maintainers
Readme
@uengage.io/js-logger
Structured observability logging for uEngage platform services. Write logs to a local file, POST them over HTTP, or emit to stdout - same JSON schema regardless of transport.
Table of Contents
- Overview
- Requirements
- Installation
- Quick Start
- Log Schema
- Initialization
- Transporters
- Log Methods
- Child Logger
- Level Filtering
- Graceful Shutdown
- Examples
- Architecture
- Running Tests
Overview
@uengage.io/js-logger provides a single Logger class with four log-level methods (info, error, debug, warn). On initialization you choose a transport:
| Transport | How it works | Best for |
| --------- | -------------------------------------------------------------- | ----------------------------------------------------------------------- |
| file | Appends NDJSON lines to {basePath}/application/{product}.log | EC2/server - CloudWatch Agent, Datadog Agent, or Fluentd ships the file |
| http | POSTs JSON to an HTTP endpoint | React Native apps, web frontends - no cloud agent available |
| stdout | Writes one NDJSON line per entry to process.stdout | Lambda, Docker - runtime captures stdout as logs |
The log schema is identical to the PHP uengage/logger package - uniform cross-service log analysis.
Requirements
- Node.js >= 18.0.0 (uses built-in
fetch,AbortSignal.timeout, andcrypto.randomUUID) - Zero runtime dependencies
Installation
Step 1 - build the TypeScript source
npm install # install devDependencies (typescript, @types/node)
npm run build # compiles src/ → dist/Step 2 - install in your project
# Local path install during development
npm install ../path/to/node
# Once published to npm
npm install @uengage.io/js-loggerStep 3 - import
const { Logger } = require('@uengage.io/js-logger');If running a script inside this repository (e.g.
examples/test.js), require directly fromdist/:const { Logger } = require('./dist/index');
Quick Start
File transporter (server / EC2)
const { Logger } = require('@uengage.io/js-logger');
const logger = new Logger({
product: 'edge',
service: 'ordering',
component: 'api-server',
version: '1.4.2',
environment: 'production',
source: 'server',
transport: { type: 'file' },
// basePath defaults to /var/log/uengage/
// log written to: /var/log/uengage/application/edge.log
});
logger.warn('Order placed', {
context: { order_id: 'ord_8x2k', amount: 450.0 },
tenant: { business_id: '456', parent_id: '123' },
user_id: 'usr_7x9k2m',
});HTTP transporter (no cloud agent)
const logger = new Logger({
product: 'edge',
service: 'ordering',
component: 'mobile-app',
version: '3.0.0',
environment: 'production',
source: 'client',
transport: {
type: 'http',
config: {
apiKey: 'your-api-key-here',
batchSize: 5,
flushIntervalMs: 5000,
},
},
});
logger.error('Payment webhook timeout', {
error: {
code: 'PAYMENT_WEBHOOK_TIMEOUT',
category: 'engineering',
upstream: 'razorpay',
stack: err.stack,
},
context: { order_id: 'ord_8x2k', latency_ms: 30012 },
tenant: { business_id: '456', parent_id: '123' },
});Stdout transporter (Lambda / Docker)
const logger = new Logger({
product: 'edge',
service: 'ordering',
component: 'worker',
version: '1.0.0',
environment: 'production',
source: 'server',
transport: { type: 'stdout' },
});Log Schema
{
"timestamp": "2026-04-07T14:32:01.847Z",
"level": "ERROR",
"product": "edge",
"service": "ordering",
"component": "mobile-app",
"version": "1.4.2",
"environment": "production",
"trace_id": "abc-123-def-456",
"tenant": { "business_id": "456", "parent_id": "123" },
"source": "server",
"message": "Payment webhook timeout",
"user_id": "usr_7x9k2m",
"error": {
"code": "PAYMENT_WEBHOOK_TIMEOUT",
"category": "engineering",
"stack": "TimeoutError: ...",
"upstream": "razorpay"
},
"context": { "order_id": "ord_8x2k", "amount": 450.0, "latency_ms": 30012 }
}timestamp…message- always presentuser_id,error,context- omitted when not passed
Initialization
const logger = new Logger(config);Validates synchronously; throws TypeError immediately if anything required is missing. If new Logger(...) completes without throwing, the instance is ready.
Config Reference
{
// ── Required ────────────────────────────────────────────────────────
product: string, // e.g. 'edge'
service: string, // e.g. 'ordering'
component: string, // e.g. 'mobile-app'
version: string, // e.g. '1.4.2'
environment: string, // 'production' | 'staging' | 'development'
source: string, // 'server' | 'client'
transport: {
type: 'file' | 'http' | 'stdout', // Required
config?: { ... }, // Optional - all fields have defaults
},
// ── Optional ────────────────────────────────────────────────────────
minLevel?: 'debug' | 'info' | 'warn' | 'error', // Default: 'warn'
}Transporters
File Transporter
Appends one NDJSON line per entry to {basePath}/application/{product}.log. The directory is created automatically; all services for the same product on a host share one file.
transport: {
type: 'file',
config: {
basePath: '/var/log/uengage', // Default: /var/log/uengage/
maxFileSizeBytes: 10 * 1024 * 1024, // Default: 10 MB
maxRotations: 5, // Default: 5
},
}File rotation - when the file reaches maxFileSizeBytes:
edge.log → edge.log.1 (previous live file)
edge.log.1 → edge.log.2
...
edge.log.5 → deletedConfigure your cloud agent to watch application/edge.log* to pick up rotated files. Writes are deferred to the next event loop tick via setImmediate.
HTTP Transporter
POSTs log entries to an HTTP endpoint using the built-in fetch API - no extra dependencies.
transport: {
type: 'http',
config: {
endpoint: 'https://observability.platform.uengage.in/logs', // Default
apiKey: 'your-api-key', // Optional. Sent as x-api-key header.
batchSize: 5, // Default: 5
flushIntervalMs: 5000, // Default: 5000 ms
timeoutMs: 5000, // Default: 5000 ms
},
}- Immediate mode (
batchSize: 1,flushIntervalMs: 0) - one POST per call; body is a plain JSON object. - Batch mode (default) - entries queue and flush when the batch is full or
flushIntervalMsfires; body is a JSON array. - Network errors and non-2xx responses are written to
process.stderrwith prefix[uengage-logger][http]; the host application is never interrupted.
Stdout Transporter
Writes one NDJSON line per entry to process.stdout. No config knobs.
transport: { type: 'stdout' },Best for AWS Lambda and Docker - the runtime captures stdout into CloudWatch Logs or your log-aggregation service. Writes are synchronous.
Log Methods
Method Signature
logger.info (message, options?)
logger.error(message, options?)
logger.debug(message, options?)
logger.warn (message, options?)Log Options Reference
{
trace_id?: string, // UUID for distributed tracing. Auto-generated via crypto.randomUUID() if not provided.
user_id?: string, // Omitted from the entry when not provided.
tenant?: {
business_id: string,
parent_id: string,
},
error?: {
code: string, // Machine-readable error code
category: string, // 'business' | 'engineering'
stack?: string, // Stack trace, e.g. err.stack
upstream?: string, // External service that caused the error
},
// Include for error and warn events. Omitted when not provided.
context?: Record<string, unknown>,
// Arbitrary key-value pairs. Deep-cloned at log time via structuredClone(). Omitted when not provided.
}Child Logger
child() creates a derived logger that shares the parent's transporter and metadata but merges in default options applied to every log call - useful for adding a per-request trace_id or tenant once rather than on every call.
const reqLogger = logger.child({
trace_id: req.headers['x-trace-id'],
tenant: { business_id: req.tenant.id, parent_id: req.tenant.parentId },
});
reqLogger.warn('Order placed', { context: { order_id: 'ord_8x2k' } });
// trace_id and tenant are automatically merged into every entry- Calling
reqLogger.destroy()is a no-op - only the parent owns the transporter. - Options passed directly to a log call override child defaults.
Level Filtering
Set minLevel to suppress low-priority logs without changing call sites (default: 'warn'):
const logger = new Logger({ ..., minLevel: 'warn' });| minLevel | DEBUG | INFO | WARN | ERROR |
| ---------------------- | :---: | :--: | :--: | :---: |
| 'warn' (default) | - | - | ✓ | ✓ |
| 'info' | - | ✓ | ✓ | ✓ |
| 'error' | - | - | - | ✓ |
| 'debug' | ✓ | ✓ | ✓ | ✓ |
Graceful Shutdown
| Transport | Action needed |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| file | None for normal exit - setImmediate writes drain before the event loop exits. Call logger.destroy() only before forced process.exit(). |
| http (immediate) | logger.destroy() recommended for best-effort delivery on shutdown. |
| http (batch, default) | Always call logger.destroy() before exit - flushes the remaining queue. |
| stdout | None - writes are synchronous. |
process.on('SIGTERM', () => {
logger.destroy();
process.exit(0);
});
process.on('SIGINT', () => {
logger.destroy();
process.exit(0);
});Examples
Business event - order placed
logger.warn('Order placed', {
trace_id: req.headers['x-trace-id'],
user_id: req.user.id,
tenant: { business_id: '456', parent_id: '123' },
context: { order_id: 'ord_8x2k', amount: 450.0, items: 3 },
});{
"timestamp": "2026-04-07T14:30:00.000Z",
"level": "WARN",
"product": "edge",
"service": "ordering",
"component": "api-server",
"version": "1.4.2",
"environment": "production",
"trace_id": "abc-123-def-456",
"tenant": { "business_id": "456", "parent_id": "123" },
"source": "server",
"message": "Order placed",
"user_id": "usr_7x9k2m",
"context": { "order_id": "ord_8x2k", "amount": 450.0, "items": 3 }
}Engineering error - payment gateway timeout
try {
await razorpay.capturePayment(payload);
} catch (err) {
logger.error('Payment webhook timeout', {
trace_id: req.headers['x-trace-id'],
user_id: req.user.id,
tenant: { business_id: '456', parent_id: '123' },
error: {
code: 'PAYMENT_WEBHOOK_TIMEOUT',
category: 'engineering',
stack: err.stack,
upstream: 'razorpay',
},
context: { order_id: 'ord_8x2k', amount: 450.0, latency_ms: 30012 },
});
}Warning - rate limit approaching
logger.warn('Rate limit approaching', {
tenant: { business_id: '456', parent_id: '123' },
error: { code: 'RATE_LIMIT_NEAR_THRESHOLD', category: 'engineering' },
context: {
endpoint: '/v1/orders',
requests_remaining: 12,
window_resets_at: '2026-04-07T15:00:00Z',
},
});Debug - database query
logger.debug('DB query executed', {
context: { table: 'orders', duration_ms: 45, rows_returned: 1 },
});Architecture
Logger
├── constructor(config) validates config, freezes metadata, creates transporter
├── _log() builds entry, applies minLevel gate
│ ├── level.toUpperCase()
│ ├── crypto.randomUUID() auto trace_id when not supplied
│ └── structuredClone() isolates context/error/tenant objects
├── child(options) shared transporter + merged default options; destroy() is no-op
└── _transporter.send(entry)
├── FileTransporter
│ ├── path: {basePath}/application/{product}.log (dir auto-created)
│ └── setImmediate → appendFileSync (NDJSON line)
│ └── _rotate() when file exceeds maxFileSizeBytes
├── HttpTransporter
│ ├── immediate: fetch(entry) one POST per call
│ └── batching: queue → flush
│ triggered by batchSize threshold or flushIntervalMs timer
└── StdoutTransporter
└── process.stdout.write(JSON.stringify(entry) + '\n')Error contract: every transporter catches all internal errors and writes to process.stderr. A logging failure never throws to the caller.
Running Tests
npm test
# or directly
node --test test/index.test.jsExpected output:
▶ Logger constructor validation
✔ throws when product is missing
✔ throws when service is missing
✔ throws when source is missing
✔ throws when transport is missing
✔ throws when transport.type is invalid
✔ Logger constructor validation
▶ File path derivation
✔ creates log file at {basePath}/application/{product}.log
✔ File path derivation
▶ Auto directory creation
✔ creates nested basePath directory if it does not exist
✔ Auto directory creation
▶ Log entry shape
✔ produces exact schema shape for an error log
✔ level is uppercase for all log methods
✔ omits user_id when not provided
✔ omits error object when not provided
✔ omits context when not provided
✔ auto-generates trace_id as a UUID when not provided
✔ Log entry shape
▶ minLevel filtering
✔ default minLevel is warn - drops DEBUG and INFO, passes WARN and ERROR
✔ minLevel debug passes all four levels
✔ minLevel filtering
▶ Context isolation (structuredClone)
✔ mutating context after log call does not affect the logged entry
✔ mutating error object after log call does not affect the logged entry
✔ Context isolation (structuredClone)
ℹ tests 17
ℹ pass 17
ℹ fail 0