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

@japfa/jarvis-apm

v1.0.0

Published

Lightweight APM + audit trail for Node.js. HTTP tracking, latency, error capture, custom audit logs, and embedded ClickHouse collector — all in one package.

Readme

apm-lite-audit

Lightweight APM + audit trail for Node.js — HTTP tracking, latency, error capture, custom audit logs, and an embedded ClickHouse collector, all in one package.

Supports: Express · Fastify · NestJS
Storage: ClickHouse (auto schema, auto TTL)
Node.js: >= 16.0.0


Installation

npm install apm-lite-audit

Quick Start

Express

const express = require('express');
const apm     = require('apm-lite-audit');

apm.init({
  serviceName: 'my-service',
  collector: {
    port: 3100,
    clickhouse: {
      host:     'http://localhost:8123',
      username: 'default',
      password: process.env.CLICKHOUSE_PASSWORD,
    },
  },
});

const app = express();
app.use(apm.middleware()); // must be before routes

app.get('/', (req, res) => res.json({ ok: true }));
app.listen(3000);

NestJS

// main.ts
import { apm } from 'apm-lite-audit/nestjs';

apm.init({
  serviceName: 'my-service',
  collector: {
    port: 3100,
    clickhouse: { host: 'http://localhost:8123', password: process.env.CLICKHOUSE_PASSWORD },
  },
});

// app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ApmInterceptor }  from 'apm-lite-audit/nestjs';

@Module({
  providers: [{ provide: APP_INTERCEPTOR, useClass: ApmInterceptor }],
})
export class AppModule {}

ClickHouse setup: docker compose up -d — a docker-compose.yml is included in this package. The schema is created automatically on first run.


Configuration

apm.init(options)

apm.init({
  // ── Required ────────────────────────────────
  serviceName: 'payment-service',

  // ── Optional ────────────────────────────────
  environment:       'production',  // default: 'production'
  sampleRate:        1.0,           // 0–1, fraction of HTTP requests to keep. Errors always kept.
  ignorePaths:       ['/health', '/metrics'],
  maxAuditPerSecond: 100,           // rate-limit apm.log() — 0 = unlimited
  timeout:           5000,          // transport HTTP timeout (ms)
  maxRetries:        3,
  maxBatchSize:      50,
  flushInterval:     2000,          // flush every N ms
  maxQueueSize:      2000,

  // ── Embedded collector ──────────────────────
  // Omit to use `endpoint` instead (external collector).
  collector: {
    port:     3100,
    database: 'apm',      // default: 'apm'
    table:    'traces',   // default: 'traces'
    ttlDays:  90,         // auto-delete rows older than N days
    clickhouse: {
      host:     'http://clickhouse:8123',
      username: 'default',
      password: process.env.CLICKHOUSE_PASSWORD,
    },
  },
});

apm.initFromConfig()

Load config from apm.config.js at your project root:

// apm.config.js
module.exports = {
  serviceName: process.env.SERVICE_NAME || 'my-service',
  environment: process.env.NODE_ENV     || 'production',
  sampleRate:  parseFloat(process.env.APM_SAMPLE_RATE || '1.0'),
  ignorePaths: ['/health', '/metrics'],
  collector: {
    port:     parseInt(process.env.APM_COLLECTOR_PORT || '3100'),
    database: process.env.CLICKHOUSE_DATABASE || 'apm',
    table:    process.env.CLICKHOUSE_TABLE    || 'traces',
    ttlDays:  90,
    clickhouse: {
      host:     process.env.CLICKHOUSE_HOST     || 'http://localhost:8123',
      username: process.env.CLICKHOUSE_USER     || 'default',
      password: process.env.CLICKHOUSE_PASSWORD || '',
    },
  },
};
const apm = require('apm-lite-audit');
apm.initFromConfig(); // reads apm.config.js from process.cwd()
app.use(apm.middleware());

HTTP Tracking

Express — apm.middleware()

app.use(apm.middleware()); // place before routes

Automatically captures every request: trace ID, latency, status, errors. Enrichment fields are picked up from req when the response finishes:

// Set in your auth middleware
req.userId    = user.id;                          // → user_id
req.tenantId  = user.tenantId;                    // → tenant_id
req.requestId = req.headers['x-request-id'];      // → request_id
req.apmMeta   = { role: user.role };              // → metadata{}
req.apmError  = err.message;                      // → error_message (on 5xx)

req.traceId is injected by the middleware — use it in apm.log() to link audit events to the HTTP trace.

Fastify — apm.fastifyPlugin()

fastify.register(apm.fastifyPlugin());
// then use request.userId, request.tenantId, request.apmMeta, etc.

NestJS Instrumentation

Import from apm-lite-audit/nestjs.

ApmInterceptor — automatic HTTP span per controller

Registered as a global interceptor, it creates a span for every controller method call, linked to the active trace.

// app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ApmInterceptor }  from 'apm-lite-audit/nestjs';

@Module({
  providers: [{ provide: APP_INTERCEPTOR, useClass: ApmInterceptor }],
})
export class AppModule {}

@Traceable() — function-level spans on services

Wraps every method on the decorated class with an APM child span, giving you a full call-stack trace per request.

import { Injectable } from '@nestjs/common';
const { Traceable } = require('apm-lite-audit/nestjs');

@Traceable()
@Injectable()
export class AgentService {
  async createAgent(dto: CreateAgentDto) { ... }
  async deleteAgent(id: string) { ... }
  // all methods are automatically traced
}

Decorate with @Traceable() above @Injectable(). Both sync and async methods are supported.

setApm(apm) — wire APM into the NestJS module

Call this once after apm.init() so the interceptor and @Traceable() can record spans:

import apm               from 'apm-lite-audit';
import { setApm }        from 'apm-lite-audit/nestjs';

apm.init({ ... });
setApm(apm);

Or if you use apm.initFromConfig():

apm.initFromConfig();
setApm(apm);

Audit Logging — apm.log(data)

Emit a business event. Always stored — bypasses HTTP sampling.

apm.log({
  event:      'payment.completed',  // required — use dot notation
  user_id:    req.userId,
  trace_id:   req.traceId,          // links this event to the HTTP trace
  request_id: req.requestId,

  // any extra fields → stored as metadata{}
  amount:   99000,
  currency: 'IDR',
});

| Field | Description | |---|---| | event | Event name, e.g. 'agent.created', 'user.login' | | user_id | User who triggered the event | | tenant_id | Tenant / organization | | trace_id | Link to the HTTP trace (req.traceId) | | request_id | Request identifier | | ...rest | Stored as metadata{} (max 2 KB) |

NestJS: @Audit() decorator

Instead of calling apm.log() manually, use the @Audit() decorator on controller methods together with AuditInterceptor to log business events automatically.

1. Create the decorator:

// src/common/decorators/audit.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const AUDIT_EVENT_KEY = 'audit_event';
export const Audit = (event: string) => SetMetadata(AUDIT_EVENT_KEY, event);

2. Create the interceptor:

// src/common/interceptors/audit.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { tap } from 'rxjs/operators';
import { AUDIT_EVENT_KEY } from '../decorators/audit.decorator';
const apm = require('apm-lite-audit');

@Injectable()
export class AuditInterceptor implements NestInterceptor {
  constructor(private readonly reflector: Reflector) {}

  intercept(ctx: ExecutionContext, next: CallHandler) {
    const event = this.reflector.getAllAndOverride<string>(AUDIT_EVENT_KEY, [
      ctx.getHandler(), ctx.getClass(),
    ]);
    if (!event) return next.handle();

    const req = ctx.switchToHttp().getRequest();
    return next.handle().pipe(
      tap({ next: () => {
        apm.log({
          event,
          user_id:    req.user?.userId,
          trace_id:   req.traceId,
          request_id: req.requestId,
          resource_id: req.params?.id,
        });
      }}),
    );
  }
}

3. Register globally and use on controllers:

// app.module.ts — add AuditInterceptor alongside ApmInterceptor
providers: [
  { provide: APP_INTERCEPTOR, useClass: ApmInterceptor },
  { provide: APP_INTERCEPTOR, useClass: AuditInterceptor },
]

// agents.controller.ts
@Post()
@Audit('agent.created')
createAgent(@Body() dto: CreateAgentDto) { ... }

@Put(':id')
@Audit('agent.updated')
updateAgent(@Param('id') id: string, @Body() dto: UpdateAgentDto) { ... }

@Delete(':id')
@Audit('agent.deleted')
deleteAgent(@Param('id') id: string) { ... }

Utility Methods

apm.stats()

app.get('/internal/apm-stats', (_req, res) => res.json(apm.stats()));
// { queueSize: 3, droppedEvents: 0, sentEvents: 1482, failedBatches: 0 }

apm.flush()

Force-flush all pending events. Useful in tests or graceful shutdown:

process.on('SIGTERM', async () => {
  await apm.flush();
  process.exit(0);
});

Distributed Tracing

Pass x-trace-id between services. apm.middleware() reads it automatically, so all spans share the same trace ID:

// Service A — outbound call
await fetch('http://inventory/reserve', {
  headers: { 'x-trace-id': req.traceId },
});
// Service B — req.traceId === same ID from Service A

ClickHouse Schema

Created automatically on first startup. For reference:

CREATE TABLE IF NOT EXISTS `apm`.`traces` (
  trace_id       String,
  type           LowCardinality(String)    DEFAULT 'http',
  service_name   LowCardinality(String),
  environment    LowCardinality(String)    DEFAULT 'production',
  endpoint       Nullable(String),
  method         LowCardinality(Nullable(String)),
  status         Nullable(UInt16),
  latency        Nullable(Float32),
  event          Nullable(String),
  user_id        Nullable(String),
  tenant_id      Nullable(String),
  request_id     Nullable(String),
  error          UInt8                     DEFAULT 0,
  error_message  Nullable(String),
  metadata       String                    DEFAULT '{}',
  timestamp      DateTime64(3, 'UTC')      DEFAULT now64()
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (service_name, timestamp, trace_id)
TTL toDateTime(timestamp) + INTERVAL 90 DAY;

Common queries:

-- Slowest endpoints (last hour)
SELECT endpoint, avg(latency) AS avg_ms, count() AS hits
FROM apm.traces
WHERE type = 'http' AND timestamp > now() - INTERVAL 1 HOUR
GROUP BY endpoint ORDER BY avg_ms DESC LIMIT 20;

-- All errors (last 24 hours)
SELECT timestamp, endpoint, status, error_message, user_id
FROM apm.traces
WHERE error = 1 AND timestamp > now() - INTERVAL 1 DAY
ORDER BY timestamp DESC;

-- Audit trail for a user
SELECT timestamp, event, metadata
FROM apm.traces
WHERE type = 'audit' AND user_id = '42'
ORDER BY timestamp DESC LIMIT 100;

-- Full trace
SELECT * FROM apm.traces
WHERE trace_id = 'abc123'
ORDER BY timestamp;

Environment Variables

| Variable | Description | |---|---| | APM_DEBUG=true | Enable verbose debug logs to stderr | | SERVICE_NAME | Service name (used in apm.config.js) | | NODE_ENV | Environment (used in apm.config.js) | | APM_SAMPLE_RATE | Sample rate 0–1 (used in apm.config.js) | | APM_COLLECTOR_PORT | Embedded collector port (used in apm.config.js) | | CLICKHOUSE_HOST | ClickHouse HTTP URL | | CLICKHOUSE_DATABASE | ClickHouse database name | | CLICKHOUSE_USER | ClickHouse username | | CLICKHOUSE_PASSWORD | ClickHouse password |


License

MIT