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

@inso_web/els-express

v0.5.2

Published

Express middleware for @inso_web/els-client with request-scoped logging (req.log) and an error handler. A drop-in replacement for pino-http and morgan + winston.

Readme

@inso_web/els-express

npm version npm downloads TypeScript license MIT

Express middleware for the Inso Error Logs Service (ELS) — a managed SaaS for centralised event logging (debug → fatal) with AI-assisted error triage. Adds req.log, automatic request logging, and an error handler that ships unhandled exceptions to ELS. Drop-in replacement for pino-http + morgan + Sentry's Express middleware with zero runtime dependencies.

🇷🇺 Русская версия → README_RU.md


Table of contents


What you get

ELS ships with a built-in admin dashboard. Every event captured by this SDK lands there with full-text search, faceted filtering, AI-assisted diagnosis, and version-aware regression detection.

| | | |---|---| | Logs list | Event detail | | Virtual table with facet sidebar (app, env, version, source, level, browser, IP, category). Live mode auto-refreshes every 5s. | Full event metadata: timestamps, geo, env, app version, fingerprint, session, repetition cards, in-session correlation. | | AI diagnosis | Analytics | | Parsed stack trace + AI-assisted diagnosis: what broke, where, how to fix. | Timeline, donuts, top URLs/IPs, hourly heatmap, version-regression widget. |


Install

npm install @inso_web/els-client @inso_web/els-express

@inso_web/els-client is a peer dependency that creates the actual client; els-express is the Express wrapper.

Requirements: Node.js 18+, Express 4 or 5.


Quick Start

import express from 'express';
import { ELSClient } from '@inso_web/els-client';
import { createELSExpressLogger, createELSErrorHandler } from '@inso_web/els-express';

const client = new ELSClient({
  apiKey: process.env.ELS_API_KEY!,
  appSlug: 'my-app',
  serviceName: 'api',
  deploymentEnv: 'PRODUCTION',
  appVersion: process.env.BUILD_VERSION,
});

const app = express();

// 1. Middleware: req.log + automatic request log
app.use(createELSExpressLogger({
  client,
  ignorePaths: [/^\/health/],
  autoLogRequests: true,        // → GET /api/users → 200 (45ms)
}));

// 2. Handlers use req.log — already bound to requestId
app.get('/api/users/:id', (req, res) => {
  req.log.info({ userId: req.params.id }, 'Fetching user');
  // ...
});

// 3. Global error handler for unhandled exceptions (LAST in the chain)
app.use(createELSErrorHandler(client));

app.listen(3000);

Don't have an API key yet? Sign up at lk.insoweb.ru — takes under a minute.

After this, in the ELS dashboard you see:

  • Every request tagged with requestId (filterable).
  • Everything that went through req.log as a structured event.
  • All unhandled errors with stack trace and AI-assisted diagnosis.

When to use the middleware vs manual capture

| Scenario | Use | |---|---| | Want per-request requestId and an out-of-the-box request log | createELSExpressLogger({ client }) | | Already have a tracing system that sets x-request-id | createELSExpressLogger({ client, requestIdHeader: 'x-request-id' }) | | Don't want auto-request rows, only manual req.log.* | createELSExpressLogger({ client, autoLogRequests: false }) | | Long-running jobs outside the request cycle | client.error(...) / client.info(...) directly | | Cron / queue worker in the same process | Reuse the client, skip the middleware | | Need a custom request-finish strategy (filter 4xx, sample 2xx) | autoLogRequests: false + your own res.on('finish') |

The middleware never throws on failure to send — it falls back to console.error so your route handlers remain safe.


Core concepts

Request-scoped logger

req.log is an ELSClient.child({ requestId, ... }) — the bindings travel with every subsequent req.log.*(...) call inside the request.

Two middlewares, two roles

createELSExpressLogger is a request middleware: it runs early and decorates req. createELSErrorHandler is an error middleware with the four-arg signature (err, req, res, next): Express only forwards errors to it. Always register the error handler after all routes and other middlewares.

Sync surface, async transport

req.log.error(err, 'failed') returns synchronously. The actual HTTP POST happens off-band. The host process is never blocked on the network.


Configuration

createELSExpressLogger(options)

| Option | Type | Default | Description | |---|---|---|---| | client | ELSClient | — | ELS client instance (required) | | ignorePaths | Array<string \| RegExp> | [] | Paths the middleware skips | | autoLogRequests | boolean | true | Log each request (METHOD URL → STATUS (Xms)) | | requestIdHeader | string | 'x-request-id' | Pre-existing request-id header to honour | | genRequestId | () => string | UUID v4 | Generator when the header is absent |

After mounting:

declare global {
  namespace Express {
    interface Request {
      log: import('@inso_web/els-client').Logger;
      id: string; // requestId
    }
  }
}

createELSErrorHandler(client)

Express error handler. Use last in the middleware chain. Captures:

  • message, stack, url, method, statusCode
  • requestId if req.id is set
  • level: 'critical' for 5xx, 'error' otherwise

Migration

From morgan

morgan ships access logs to stdout. ELS ships structured events to a queryable dashboard.

Before:

import express from 'express';
import morgan from 'morgan';

const app = express();
app.use(morgan('combined'));
app.use(morgan('tiny', {
  skip: (_req, res) => res.statusCode < 400,
}));

After:

import express from 'express';
import { ELSClient } from '@inso_web/els-client';
import { createELSExpressLogger } from '@inso_web/els-express';

const client = new ELSClient({ apiKey, appSlug: 'my-app' });
const app = express();
app.use(createELSExpressLogger({
  client,
  autoLogRequests: true,        // analogous to morgan('tiny')
  ignorePaths: [/^\/health/],
}));

| morgan concept | ELS equivalent | Notes | |---|---|---| | morgan('combined') | autoLogRequests: true | One structured event per request | | morgan('tiny') | autoLogRequests: true | Same idea, no format strings | | skip: (req, res) => ... | ignorePaths + custom res.on('finish') | More granular skip needs manual handler | | :response-time token | Captured automatically | Field meta.duration | | :res[x-trace-id] token | req.id / requestId | Auto-generated UUID if missing |

Gotchas:

  • morgan writes plain text to stdout; ELS sends JSON to the network — you lose tail -f access. Keep one of: pm2 logs, journalctl, or a separate stdout transport for ops shells.
  • Custom tokens don't translate one-to-one — use req.log.info({ field }, ...) instead.

From pino-http

API surface is intentionally close. The big difference: no pino peer dependency, no separate transport package.

Before:

import express from 'express';
import pinoHttp from 'pino-http';
import pino from 'pino';

const logger = pino({ level: 'info' });
const app = express();
app.use(pinoHttp({ logger, genReqId: () => crypto.randomUUID() }));

app.get('/users/:id', (req, res) => {
  req.log.info({ userId: req.params.id }, 'fetched');
  res.send('ok');
});

After:

import express from 'express';
import { ELSClient } from '@inso_web/els-client';
import { createELSExpressLogger } from '@inso_web/els-express';

const client = new ELSClient({ apiKey, appSlug: 'my-app', minLevel: 'info' });
const app = express();
app.use(createELSExpressLogger({ client }));

app.get('/users/:id', (req, res) => {
  req.log.info({ userId: req.params.id }, 'fetched');
  res.send('ok');
});

| pino-http | ELS | Notes | |---|---|---| | pinoHttp({ logger }) | createELSExpressLogger({ client }) | Same role | | req.log | req.log | Same name, same API | | genReqId | genRequestId | Same idea | | pinoHttp.startTime | req.id is generated lazily | No need to set manually | | serializers.req/res | Pre-shape in BeforeSend or req.log.info(...) | No serializer option | | Transport package (e.g. pino-loki) | Not needed | HTTP transport is built-in |

Gotchas:

  • pino-http logs requests at info by default; ELS does the same when autoLogRequests: true. To match a stricter logger, bump minLevel to 'warn'.
  • pino-http's customLogLevel(req, res, err) has no direct equivalent — implement via autoLogRequests: false + a custom res.on('finish') finisher.

From @sentry/node (Express middleware)

Before:

import express from 'express';
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: 'https://[email protected]/1',
  environment: 'production',
  release: process.env.BUILD_VERSION,
});

const app = express();
app.use(Sentry.Handlers.requestHandler());
// ... routes ...
app.use(Sentry.Handlers.errorHandler());

After:

import express from 'express';
import { ELSClient } from '@inso_web/els-client';
import { createELSExpressLogger, createELSErrorHandler } from '@inso_web/els-express';

const client = new ELSClient({
  apiKey: process.env.ELS_API_KEY!,
  appSlug: 'my-app',
  deploymentEnv: 'PRODUCTION',
  appVersion: process.env.BUILD_VERSION,
});

const app = express();
app.use(createELSExpressLogger({ client }));
// ... routes ...
app.use(createELSErrorHandler(client));

| Sentry | ELS | Notes | |---|---|---| | Sentry.Handlers.requestHandler() | createELSExpressLogger({ client }) | Same position in the chain | | Sentry.Handlers.errorHandler() | createELSErrorHandler(client) | Same position (last) | | Sentry.Handlers.tracingHandler() | Not provided | ELS does not do tracing | | dsn | apiKey + appSlug | Three explicit fields | | environment | deploymentEnv | Fixed enum | | release | appVersion | Any string ≤128 chars | | Source maps upload | Not provided | Pair with Sentry if critical |

Gotchas:

  • ELS does not perform tracing — drop tracingHandler. If you rely on Sentry Performance, keep it alongside.
  • Sentry's per-request scope tagging maps cleanly to req.log.child({ ...tags }).

Versioning

Pass BUILD_VERSION through Dockerfile and CI. ELS accepts any string ≤128 chars: semver, CalVer, date-compact (YYYYMMDDHHmmss), git SHA, opaque.

ARG BUILD_VERSION=dev
ENV BUILD_VERSION=$BUILD_VERSION
# .gitlab-ci.yml
- export BUILD_VERSION=$(date -u +%Y%m%d%H%M%S)
- docker build --build-arg BUILD_VERSION="$BUILD_VERSION" ...
new ELSClient({ ..., appVersion: process.env.BUILD_VERSION });

In the dashboard you get a "Regressions" widget: "this error first seen in v20260507120000, not present in v20260506180000."


Quick reference

| Need | Use | |---|---| | Per-request logger | req.log.info({ ... }, '...') | | Skip health checks | ignorePaths: [/^\/health/] | | Honour upstream trace id | requestIdHeader: 'x-trace-id' | | Suppress 4xx noise | autoLogRequests: false + custom res.on('finish') | | Capture unhandled errors | app.use(createELSErrorHandler(client)) (last) | | Process-level crashes | process.on('uncaughtException', ...) (see below) | | One client across modules | Export a singleton from lib/els.ts |


Why ELS

ELS for Node.js is a focused logging SaaS, not a full observability suite. It optimises for capture speed, AI-driven triage, and a low integration cost.

  • Lower weight. No transitive deps in the middleware; one dependency-free package on Node.
  • Zero external API calls. Only POST /errors[/batch] and GET /health.
  • AI-assisted diagnosis on every stack trace — no add-ons, no extra setup.
  • 5-minute integration. Install → set API key → done.
  • Predictable price. Tariffs live in the dashboard.

Detailed comparison

| Category | ELS | Sentry | Datadog / New Relic | Grafana Loki | LogRocket / Logtail / BetterStack | |---|---|---|---|---|---| | Hosting model | Managed SaaS | SaaS or self-hosted | SaaS only | Self-hosted / Grafana Cloud | SaaS | | SDK runtime deps | Zero | Medium (sub-SDKs, integrations) | Heavy (agent + tracing) | Promtail / agent | Medium | | Typical integration time | ~5 min | 10–20 min | 30–60 min | Hours to days | 10–20 min | | AI-assisted triage | Built-in | Paid add-on | Paid add-on | None | None | | Error grouping / fingerprint | Yes | Yes | Yes | Manual via LogQL | Partial | | Source-map upload | No | Yes | Yes | n/a | Partial | | Session replay (frontend) | No | Paid | Paid | n/a | Yes (core) | | Distributed tracing / APM | No | Partial | Yes (core) | Yes with Tempo | No | | Infrastructure metrics | No | No | Yes (core) | Yes with Mimir | No | | Free tier log retention | 24 hours | 30 days (limited volume) | Trial only | Self-cost | 3–30 days | | Russian-language support / docs | Native | Community | Limited | Community | None |

When ELS is the wrong choice

  • You need a single vendor for APM + logs + metrics under one bill — go Datadog or New Relic.
  • Your frontend bug triage relies on DOM session replay — go LogRocket or Sentry Replay.
  • You ship a public mobile app and need crash symbolication + ANR detection — Firebase Crashlytics or Sentry Mobile.

For everything else — backend errors, frontend JS errors, request logs, structured app events with version-aware analytics — ELS is built to be the cheapest path to a working dashboard.

Sign up at lk.insoweb.ru to grab an API key.


Process-level handlers

Express does not catch unhandledRejection and uncaughtException — you need global handlers:

process.on('unhandledRejection', (reason) => {
  const err = reason instanceof Error ? reason : new Error(String(reason));
  client.error(err, '[unhandledRejection]');
});

process.on('uncaughtException', (err, origin) => {
  client.fatal({ err, origin }, '[uncaughtException]');
});

Tip: a tail flush on exit prevents losing the last batch in the queue:

process.on('beforeExit', () => client.flush());

API

function createELSExpressLogger(opts: {
  client: ELSClient;
  ignorePaths?: Array<string | RegExp>;
  autoLogRequests?: boolean;
  requestIdHeader?: string;
  genRequestId?: () => string;
}): express.RequestHandler;

function createELSErrorHandler(client: ELSClient): express.ErrorRequestHandler;

Full ELSConfig reference — see @inso_web/els-client.


Other ELS SDKs

Same wire format, same dashboard — pick by stack.

Node.js family

Other stacks


Pricing

Free tier — 24-hour log retention. See lk.insoweb.ru for the full tariff matrix.


License

MIT © INSOWEB