JS Logs Interceptor

Elven Observability Official Logger

A high-performance log, event, and exception collector for Node.js apps that sends everything to Grafana Loki. It offers automatic console interception, support for dynamic labels, asynchronous sending with optimized buffering, and instant integration with the Elven Stack.

Features

High performance with asynchronous buffer, batch sending, and gzip compression.

Automatically captures:

  • console.log, console.error, console.warn, etc

  • Unhandled exceptions and rejected promises

  • Custom event via trackEvent

Support for static and dynamic labels (with trace/span ID via OpenTelemetry).

Smart buffer with configurable batch sending.

Resilient to network failures with automatic retry.

Compatible with both simple and complex apps (web servers, workers, CLI).

Delivery guarantee on process shutdown.

Gzip compression to reduce bandwidth usage.

Real-time metrics and integrated health check.

Installation

npm install logs-interceptor

Basic Usage

import { init, logger } from "logs-interceptor";

init({
  transport: {
    url: "<https://loki.elvenobservability.com/loki/api/v1/push>",
    tenantId: "your-tenant", // Your Tenant ID
    authToken: "your-token", // Your JWT
    compression: true, // Gzip enabled
  },
  appName: "my-js-app",
  environment: "production",
  interceptConsole: true,
  labels: { env: "production" },
  dynamicLabels: {
    hostname: () => require('os').hostname(),
    memory: () => Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + 'MB'
  },
  buffer: {
    maxSize: 100,
    flushInterval: 5000,
  }
});

console.log("This will be sent to Loki!");
logger.trackEvent("registered_user", { user_id: 123 });
throw new Error("Test error!"); // Automatically captured

Configuration Options

Transport

Parameter
Type
Description
Default

url

string

Loki endpoint (Push API)

tenantId

string

Used in the X-Scope-OrgID header

authToken

string

JWT token for authentication

undefined

timeout

number

HTTP timeout in ms

5000

maxRetries

number

Retries in case of failure

3

compression

boolean

Enables gzip compression

true

Application

Parameter
Type
Description
Default

appName

string

Application name (fixed label)

version

string

Application version

“1.0.0”

environment

string

Environment (prod, dev, staging)

“production”

labels

object

Fixed labels per log

{}

dynamicLabels

object

Dynamically generated labels

{}

Buffer & Performance

Parameter
Type
Description
Default

buffer.maxSize

number

Logs in the buffer before forcing send

100

buffer.flushInterval

number

Maximum time before sending (ms)

5000

buffer.autoFlush

boolean

Auto flush enabled

true

interceptConsole

boolean

Automatically captures console.*

false

enableMetrics

boolean

Collects performance metrics

true

Filtering & Sampling

Parameter
Type
Description
Default

filter.levels

array

Allowed log levels

[‘debug’,’info’,’warn’,’error’,’fatal’]

filter.samplingRate

number

Sampling rate (0.0–1.0)

1.0

filter.maxMessageLength

number

Maximum message size

8192

Example with Express.js

import express from "express";
import { init, logger } from "logs-interceptor";

init({
  transport: {
    url: "<https://loki.elvenobservability.com/loki/api/v1/push>",
    tenantId: "your-tenantId",
    authToken: "your-token",
    compression: true,
  },
  appName: "express-app",
  environment: "production",
  interceptConsole: true,
  labels: {
    service: "api",
    env: "production"
  },
  dynamicLabels: {
    hostname: () => require('os').hostname(),
    uptime: () => Math.round(process.uptime()) + 's'
  },
});

const app = express();

app.get("/ping", (req, res) => {
  console.log("Ping called"); // Automatically intercepted
  logger.trackEvent("api_ping", { ip: req.ip });
  res.send({ message: "pong" });
});

app.get("/erro", () => {
  throw new Error("Intentional error"); // Automatically captured
});

app.listen(3000, () => {
  console.log("Server started on port 3000");
});

Auto-initialization with NODE_OPTIONS

For existing apps without code modification:

NODE_OPTIONS="--require logs-interceptor/preload" \\
LOGS_INTERCEPTOR_URL="<https://loki.elvenobservability.com/loki/api/v1/push>" \\
LOGS_INTERCEPTOR_TENANT_ID="your-tenant" \\
LOGS_INTERCEPTOR_APP_NAME="my-api" \\
LOGS_INTERCEPTOR_AUTH_TOKEN="your-token" \\
LOGS_INTERCEPTOR_ENVIRONMENT="production" \\
node app.js

Metrics & Monitoring

import { logger } from "logs-interceptor";

// Real-time metrics
const metrics = logger.getMetrics();
console.log({
  logsProcessed: metrics.logsProcessed,
  logsDropped: metrics.logsDropped,
  flushCount: metrics.flushCount,
  avgFlushTime: metrics.avgFlushTime,
  errorCount: metrics.errorCount
});

// Health check
const health = logger.getHealth();
console.log({
  healthy: health.healthy,
  uptime: health.uptime,
  bufferUtilization: health.bufferUtilization
});

Best Practices

Scenario
Recommendations

Short scripts (CLI, cron)

Use await logger.flush() before the exit.

Web apps or workers

The default setup already handles background sending.

High concurrency

Use maxSize: 500+ and flushInterval: 10000.

Environments with OpenTelemetry

Labels trace_id and span_id are captured automatically.

Production

Enable compression: true e samplingRate < 1.0 if needed.

Troubleshooting

Problem
Common cause
Solution

Logs are not showing up

tenantId, authToken or Incorrect URLs

Check the configuration and set debug: true."

Incomplete logs

Process terminating quickly

Use await logger.flush() before exiting

HTTP 400

Malformed payload

Check if Loki is running at the URL

Poor performance

Buffer too small

Increase maxSize and flushInterval

Manual Flush

import { logger } from "logs-interceptor";

// Force immediate send
await logger.flush();

// Exemplo com graceful shutdown
process.on('SIGTERM', async () => {
  console.log('Shutting down gracefully...');
  await logger.flush();
  process.exit(0);
});

Custom Events

import { logger } from "logs-interceptor";

logger.trackEvent("active_user", {
  user_id: "abc123",
  source: "mobile",
  action: "login"
});

logger.trackEvent("purchase_completed", {
  order_id: "12345",
  value: 99.90,
  payment_method: "credit_card"
});

In Loki, it will appear as:

[EVENT] usuario_ativo {"user_id": "abc123", "source": "mobile", "action": "login"}

Log Format in Loki

Each log sent includes:

  • Timestamp with nanosecond precision

  • Fixed and dynamic labels (e.g., app, env, trace_id, span_id)

  • Log type prefix (e.g.,[INFO], [ERROR], [EVENT], etc)

  • Content automatically serialized for objects and errors

Example of payload sent:

{
  "streams": [
    {
      "stream": {
        "app": "minha-api",
        "environment": "production",
        "level": "info",
        "hostname": "api-01",
        "trace_id": "abc123def456"
      },
      "values": [
        ["1640995200000000000", "[INFO] User successfully logged in {\\"userId\\": 123}"]
      ]
    }
  ]
}

Integration with OpenTelemetry

This logger automatically integrates with OpenTelemetry, capturing the active trace and span IDs from the current context.

This enables seamless correlation between logs and traces in Grafana Tempo, allowing for complete end-to-end investigations.

What is already included:

  • trace_id and span_id are automatically added as dynamic labels.

  • No additional configuration is required, as long as the app already uses OpenTelemetry.

  • Compatible with any existing OpenTelemetry instrumentation.

Example with traces

import { context, trace } from "@opentelemetry/api";
import { init, logger } from "logs-interceptor";

// Initialize OpenTelemetry first:
const tracer = trace.getTracer("my-lib");

init({
  transport: {
    url: "<https://loki.elvenobservability.com/loki/api/v1/push>",
    tenantId: "elven",
    authToken: "your-token"
  },
  appName: "my-app",
  labels: { env: "prod" },
});

tracer.startActiveSpan("processOrder", (span) => {
  console.log("Processing order");
  logger.info("Order started", { orderId: 12345 });
  span.end();
});

In Loki:

Each log will automatically include:

{
  "trace_id": "abcdef123456789",
  "span_id": "12345678",
  "app": "my-app",
  "level": "info"
}

You will be able to query Loki by filtering with trace_id, and with that, view the logs from the same trace shown in Grafana Tempo (or the trace view in the Elven Platform).

Docker & Kubernetes

Docker Compose

version: '3.8'
services:
  app:
    build: .
    environment:
      - NODE_OPTIONS=--require logs-interceptor/preload
      - LOGS_INTERCEPTOR_URL=https://loki.elvenobservability.com/loki/api/v1/push
      - LOGS_INTERCEPTOR_TENANT_ID=meu-tenant
      - LOGS_INTERCEPTOR_AUTH_TOKEN=meu-token
      - LOGS_INTERCEPTOR_APP_NAME=minha-api
      - LOGS_INTERCEPTOR_ENVIRONMENT=production

Kubernetes

env:
- name: NODE_OPTIONS
  value: "--require logs-interceptor/preload"
- name: LOGS_INTERCEPTOR_URL
  value: "<https://loki.elvenobservability.com/loki/api/v1/push>"
- name: LOGS_INTERCEPTOR_TENANT_ID
  valueFrom:
    secretKeyRef:
      name: elven-credentials
      key: tenant-id
- name: LOGS_INTERCEPTOR_AUTH_TOKEN
  valueFrom:
    secretKeyRef:
      name: elven-credentials
      key: auth-token

License

This project is licensed under the MIT License.

How to contribute

  1. Fork this repository

  2. Create a branch with your improvement

  3. Submit a Pull Request

Any help is welcome to make the logger even more powerful!

Made with ❤️ by the Elven Observability team.

Last updated

Was this helpful?