@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.
Maintainers
Readme
@inso_web/els-express
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.
Table of contents
- What you get
- Install
- Quick Start
- When to use the middleware vs manual capture
- Core concepts
- Configuration
- Migration
- Versioning
- Quick reference
- Why ELS
- Process-level handlers
- API
- Other ELS SDKs
- Pricing
- License
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.
| | |
|---|---|
|
|
|
| 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. |
|
|
|
| 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.logas 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,statusCoderequestIdifreq.idis setlevel: '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 -faccess. 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
infoby default; ELS does the same whenautoLogRequests: true. To match a stricter logger, bumpminLevelto'warn'. - pino-http's
customLogLevel(req, res, err)has no direct equivalent — implement viaautoLogRequests: false+ a customres.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]andGET /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
@inso_web/els-client— base TS / Node / browser client@inso_web/els-express— Express middleware (this repo)@inso_web/els-next— Next.js helpers (App + Pages router)@inso_web/els-nest— NestJS module@inso_web/els-react— React Provider, hooks, ErrorBoundary@inso_web/els-vue— Vue 3 plugin
Other stacks
Inso.Els— .NET (Core + ASP.NET Core + ILogger)io.github.official-inso:els-core— Java + Spring Boot starter + SLF4Jgithub.com/official-inso/els-go— Go
Pricing
Free tier — 24-hour log retention. See lk.insoweb.ru for the full tariff matrix.
License
MIT © INSOWEB
