Skip to content

MiniObserv Integration Guide for Node.js Projects

This guide is for developers running Express.js, NestJS, or Next.js who want real visibility into what their app is doing in production — request latency, error rates, memory pressure, disk usage — without adding a heavy observability stack.


1. What MiniObserv actually covers

Before integrating, it's important to know which observability layer MiniObserv covers — and which it doesn't.

Layer 1 — Infrastructure        ← MiniObserv
  Is the machine healthy?
  CPU, memory, disk, network, log lines
  Tools: MiniObserv, Datadog, Prometheus

Layer 2 — Application (APM)
  Is my code healthy?
  Slow queries, N+1, stack traces, latency per function
  Tools: Sentry, Datadog APM, OpenTelemetry

Layer 3 — Business
  Is my product healthy?
  Active users, conversions, feature usage
  Tools: PostHog, Mixpanel, Amplitude

MiniObserv is Layer 1. It monitors the server running your app — the OS, not your code or your data. It does not connect to your PostgreSQL, read your Prisma schema, or know anything about your business logic. When you deploy to 3 servers, it tells you each server's CPU is at 42% — it won't tell you which Prisma query caused a spike (that's Layer 2).

In production you'll eventually want all three layers. Start with Layer 1 — if the server runs out of memory or disk, nothing else matters.


2. Why MiniObserv for Node.js projects

Your framework is solid. What you don't have is visibility into what happens once it's deployed.

What you get out of the box — zero code required:

  • CPU, memory, disk, and network metrics collected every 10 seconds
  • All your app's log lines surfaced in a structured dashboard log stream
  • Alerts when system resources exceed thresholds you define
  • Live sparkline dashboard at http://localhost:8080

The MiniObserv agent is a standalone Go binary. It runs alongside your Node.js app as a separate container or process. It knows nothing about Node.js — it monitors the host OS. You don't need to install anything in your app to get system-level metrics.

The SDK (@kamerrezz/miniobserv) is optional. Use it only when you need to push custom application-level metrics (response time distributions, error counts, queue depths). Start without it — you may not need it.


2. Quick Setup (3 minutes)

Add MiniObserv to your existing docker-compose.yml. You need three new services alongside yours:

ServiceWhat it isTouches your app?
miniobserv-dbTimescaleDB — MiniObserv's own internal storageNo
miniobservThe MiniObserv server + dashboardNo
miniobserv-agentCollects OS metrics from the hostNo

miniobserv-db is completely separate from your database. If your stack uses PostgreSQL + Prisma, MySQL + Sequelize, or anything else — MiniObserv never touches it. It has its own isolated database container. Your app and MiniObserv don't share any data storage.

Typical stack: Express + Prisma + PostgreSQL

This is what a real project looks like with MiniObserv added:

yaml
services:
  # ── your existing stack ──────────────────────────────────────
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: myapp
      POSTGRES_DB: myapp_db
    volumes:
      - app_data:/var/lib/postgresql/data

  app:
    build: .
    environment:
      DATABASE_URL: "postgresql://myapp:myapp@db:5432/myapp_db"
    depends_on:
      - db
      - miniobserv        # optional: wait for dashboard to be ready
    volumes:
      - app_logs:/var/log/app

  # ── MiniObserv — add these three services ───────────────────
  miniobserv-db:          # MiniObserv's own DB — isolated from yours
    image: timescale/timescaledb:latest-pg16
    environment:
      POSTGRES_USER: minidog
      POSTGRES_PASSWORD: minidog
      POSTGRES_DB: miniobserv
    volumes:
      - miniobserv_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U minidog -d miniobserv"]
      interval: 5s
      retries: 10

  miniobserv:
    image: kamerrezz/miniobserv-server:latest
    environment:
      DATABASE_URL: "postgres://minidog:minidog@miniobserv-db:5432/miniobserv?sslmode=disable"
      AGENT_TOKEN: "your-32-char-secret-here"
      ALERT_RULES: '[{"host":"*","name":"cpu.usage_pct","op":">","threshold":80,"for":"5m"}]'
    ports:
      - "8080:8080"
    depends_on:
      miniobserv-db:
        condition: service_healthy

  miniobserv-agent:
    image: kamerrezz/miniobserv-agent:latest
    environment:
      SERVER_URL: "http://miniobserv:8080"
      AGENT_TOKEN: "your-32-char-secret-here"
      LOG_PATHS: "/var/log/app/app.log"
    volumes:
      - app_logs:/var/log/app:ro   # read-only access to your app's logs

volumes:
  app_data:           # your app's data
  miniobserv_data:    # MiniObserv's data — completely separate
  app_logs:           # shared between app (write) and agent (read)

Generate a strong AGENT_TOKEN:

bash
openssl rand -hex 32

Use the same value in both miniobserv and miniobserv-agent. It never leaves your infrastructure.

Open http://localhost:8080 after docker compose up. The dashboard appears within ~10 seconds.


3. Express.js Integration

3a. Write logs to a file (so MiniObserv can tail them)

MiniObserv's agent watches files listed in LOG_PATHS and streams every new line into the dashboard log viewer. The simplest approach is to add a file transport to your existing Winston logger:

javascript
const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: '/var/log/myapp/app.log' }),
  ],
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json(),
  ),
});

Mount the same volume in your app container so the agent can read the file:

yaml
services:
  api:
    volumes:
      - app_logs:/var/log/myapp

3b. Request logging middleware (structured format)

Log every request so MiniObserv's log stream shows meaningful HTTP activity. Log level is determined by status code — ERROR for 5xx, WARN for 4xx, INFO otherwise:

javascript
app.use((req, res, next) => {
  res.on('finish', () => {
    const level =
      res.statusCode >= 500 ? 'error' :
      res.statusCode >= 400 ? 'warn' : 'info';

    logger[level]({
      method: req.method,
      path: req.path,
      status: res.statusCode,
    });
  });
  next();
});

This is enough to see HTTP errors and slow routes in the dashboard without any SDK.

3c. Pushing custom app metrics with the SDK (optional)

Install the SDK:

bash
npm install @kamerrezz/miniobserv

Initialize the client once and reuse it across your app:

javascript
import { MiniObservClient } from '@kamerrezz/miniobserv';

const obs = new MiniObservClient({
  baseUrl: process.env.MINIOBSERV_URL ?? 'http://miniobserv:8080',
  agentToken: process.env.AGENT_TOKEN,
  defaultHost: process.env.HOSTNAME ?? 'api-server',
});

Push a custom signal from middleware — fire-and-forget, never block the request:

javascript
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const heapUsed = process.memoryUsage().heapUsed;
    const heapTotal = process.memoryUsage().heapTotal;

    obs.pushMetric('mem.used_pct', (heapUsed / heapTotal) * 100)
      .catch(() => {}); // never let observability crash the request
  });
  next();
});

A note on metric names: MiniObserv has a fixed set of canonical names (cpu.usage_pct, mem.used_pct, disk.used_pct, etc.). The agent already populates these from the OS. When pushing from the SDK, use the same names to add data points from the app's perspective — or push any value you want to track. The recommended approach for most apps is to start with the sidecar pattern (agent only), and add SDK pushes only for high-value signals like error rate or DB latency.


4. NestJS Integration

4a. Global interceptor for request logging

Create an interceptor that logs every request with method, URL, status, and duration:

typescript
// src/common/observability.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class ObservabilityInterceptor implements NestInterceptor {
  private readonly logger = new Logger('HTTP');

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const start = Date.now();

    return next.handle().pipe(
      tap(() => {
        const res = context.switchToHttp().getResponse();
        const duration = Date.now() - start;
        this.logger.log(
          `${req.method} ${req.url} → ${res.statusCode} (${duration}ms)`,
        );
      }),
    );
  }
}

4b. Register globally in main.ts

typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ObservabilityInterceptor } from './common/observability.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new ObservabilityInterceptor());
  await app.listen(3000);
}
bootstrap();

4c. Route NestJS logs to a file with Winston

bash
npm install nest-winston winston
typescript
// src/winston.config.ts
import * as winston from 'winston';

export const winstonConfig: winston.LoggerOptions = {
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: '/var/log/myapp/app.log' }),
  ],
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json(),
  ),
};
typescript
// main.ts
import { WinstonModule } from 'nest-winston';
import { winstonConfig } from './winston.config';

const app = await NestFactory.create(AppModule, {
  logger: WinstonModule.createLogger(winstonConfig),
});

With this in place, every NestJS log — including the interceptor output — lands in /var/log/myapp/app.log, which the MiniObserv agent tails automatically.


5. Next.js Integration

5a. API route middleware (App Router, Next.js 14+)

Next.js middleware runs at the Edge and does not have access to Node.js file APIs. The simplest way to get logs into MiniObserv is to write structured JSON to stdout and redirect Docker's output to a file.

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const start = Date.now();
  const response = NextResponse.next();

  console.log(
    JSON.stringify({
      level: 'INFO',
      method: request.method,
      path: request.nextUrl.pathname,
      duration_ms: Date.now() - start,
    }),
  );

  return response;
}

export const config = {
  matcher: '/api/:path*',
};

5b. Redirect Next.js stdout to a file (docker-compose)

Pipe the container's stdout to a file that the agent can tail:

yaml
services:
  nextjs:
    command: sh -c "node server.js 2>&1 | tee /var/log/myapp/app.log"
    volumes:
      - app_logs:/var/log/myapp

The tee command writes to both stdout (so you can still docker logs) and the file. The MiniObserv agent picks up new lines automatically via LOG_PATHS.


6. Alert Rules for Node.js Apps

Define alert rules as a JSON array in the ALERT_RULES environment variable on the MiniObserv server. Each rule fires when the condition holds for the specified duration:

json
[
  { "host": "*", "name": "cpu.usage_pct",  "op": ">", "threshold": 80, "for": "5m"  },
  { "host": "*", "name": "mem.used_pct",   "op": ">", "threshold": 85, "for": "10m" },
  { "host": "*", "name": "disk.used_pct",  "op": ">", "threshold": 90, "for": "1m"  }
]
  • host: "*" matches all hosts reporting to this server
  • for is the minimum sustained duration before the alert fires — avoids noise from transient spikes
  • Fired alerts appear in the dashboard; configure ALERT_NOTIFICATIONS to also receive webhook notifications (see Section 8)

A practical starting point for a typical Node.js API:

MetricThresholdForRationale
cpu.usage_pct80%5mSustained load — not a single spike
mem.used_pct85%10mMemory leaks grow slowly
disk.used_pct90%1mDisk full is immediate and fatal

8. Notifications and Host Health

8a. Webhook alert notifications

MiniObserv can notify external services when an alert fires or resolves. Set ALERT_NOTIFICATIONS on the server to a JSON array of webhook destinations:

yaml
miniobserv:
  environment:
    ALERT_NOTIFICATIONS: '[{"type":"webhook","url":"https://hooks.slack.com/services/YOUR/WEBHOOK/URL"}]'

Works with Slack, Discord, Teams, PagerDuty, or any service that accepts a JSON POST. Each notification is delivered with a 5-second timeout (fire-and-forget, no retry in v1).

Payload shape:

json
{"event":"firing","rule":{"host":"*","name":"mem.used_pct","op":">","threshold":85,"for":"10m"},"value":87.4,"fired_at":"2026-06-05T16:42:23Z"}

event is "firing" when the threshold is crossed and "resolved" when the metric drops back below it.

8b. Automatic host-down alerts

The server tracks the last heartbeat for each connected agent. If an agent stops reporting metrics — due to a container crash, OOM kill, network partition, or any other reason — MiniObserv will automatically fire a host.down webhook once the host has been silent for longer than HOST_DOWN_AFTER (default 50s).

No configuration is required beyond setting ALERT_NOTIFICATIONS. You do not need to write an alert rule for this — host health is tracked automatically.

Env varDefaultMeaning
HOST_STALE_AFTER20sHost becomes orange (stale) in the dashboard
HOST_DOWN_AFTER50sHost becomes red (down) and webhook fires

You can query the current status of all hosts via GET /api/v1/hosts (public, no auth):

bash
curl -s http://localhost:8080/api/v1/hosts | jq .

8c. Updated docker-compose snippet with notifications

yaml
miniobserv:
  build:
    context: ./path/to/theminidog
    dockerfile: Dockerfile.server
  environment:
    DATABASE_URL: "postgres://minidog:minidog@miniobserv-db:5432/miniobserv?sslmode=disable"
    AGENT_TOKEN: "your-32-char-secret-here"
    ALERT_RULES: '[{"host":"*","name":"cpu.usage_pct","op":">","threshold":80,"for":"5m"},{"host":"*","name":"mem.used_pct","op":">","threshold":85,"for":"10m"}]'
    ALERT_NOTIFICATIONS: '[{"type":"webhook","url":"https://hooks.slack.com/services/YOUR/WEBHOOK/URL"}]'
    HOST_STALE_AFTER: "20s"
    HOST_DOWN_AFTER: "50s"
  ports:
    - "8080:8080"
  depends_on:
    miniobserv-db:
      condition: service_healthy

7. What You Get Without Writing a Single Line of SDK Code

If you only add the two MiniObserv services to your docker-compose and point LOG_PATHS at your app's log file, you get:

  • System metrics — CPU, memory, disk, network collected every 10 seconds, automatically
  • Log stream — every line your app writes to the log file, visible in the dashboard with timestamps
  • Alerts — notifications when resources cross the thresholds you define
  • Live dashboard — sparkline charts at http://localhost:8080, no setup required

The SDK is for when you need to go further: track error rates by route, measure DB query latency, record queue depths. Start without it. Add it when you have a specific signal you cannot observe from logs and system metrics alone.


Reference

Canonical metric names

NameUnitDescription
cpu.usage_pct%CPU utilization across all cores
mem.used_pct%Memory used as a percentage of total
mem.used_bytesbytesMemory currently in use
mem.total_bytesbytesTotal installed memory
disk.used_pct%Disk used percentage (mount /)
disk.used_bytesbytesDisk space in use
disk.total_bytesbytesTotal disk capacity
net.bytes_inbytesNetwork bytes received (delta per tick)
net.bytes_outbytesNetwork bytes sent (delta per tick)

net.* metrics are delta values — no data is emitted on the first collection tick.

Agent environment variables

VariableRequiredDefaultDescription
SERVER_URLyesBase URL of the MiniObserv server
AGENT_TOKENyesShared HS256 secret (min 16 chars)
COLLECT_INTERVALno10sCollection frequency (1s–300s)
LOG_PATHSnoComma-separated paths to tail
AGENT_HOSTnoOS hostnameHost label on all metrics

Released under the MIT License.