npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@uengage.io/js-logger

v0.10.0

Published

Structured observability/logging for uEngage platform services

Downloads

1,279

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

@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, and crypto.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-logger

Step 3 - import

const { Logger } = require('@uengage.io/js-logger');

If running a script inside this repository (e.g. examples/test.js), require directly from dist/:

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 }
}
  • timestampmessage - always present
  • user_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 → deleted

Configure 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 flushIntervalMs fires; body is a JSON array.
  • Network errors and non-2xx responses are written to process.stderr with 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.js

Expected 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