api-observatory
v1.1.1
Published
API Traffic Metrics & Latency Percentiles — zero-dependency core with Express, Fastify, and Koa adapters
Maintainers
Readme
Add two lines of code. Get a full metrics dashboard, auto-generated API docs, and an interactive guide — all served from your running app at /_observatory.
Table of Contents
- Why I Built This
- What This Is (and Isn't)
- What You Get
- Why API Observatory?
- Quick Start
- Schema Capture (API Docs)
- Configuration
- Security
- API Endpoints
- Route Extraction
- Requirements
- License
Why I Built This
Every API starts the same way: you build endpoints, ship them, and assume they're fast enough. Then one day a user reports that "the app feels slow." You open your terminal, realize you have no idea which endpoint is the bottleneck, and start googling how to set up Prometheus with Grafana. An hour later you're writing YAML files and configuring dashboards instead of fixing the actual problem.
I wanted something I could drop into any Express/Fastify/Koa app and immediately see which endpoints are slow, what the P95 looks like, and whether error rates are climbing — without installing a single dependency, without configuring an external service, without leaving my app. Two lines of code, open a browser tab, done.
The API docs feature came next. I was tired of maintaining OpenAPI specs that drifted from reality within a week. If the middleware is already watching every request and response, why not infer the schemas automatically? So it does — you get Swagger-like docs generated from your live traffic, always accurate, always current.
What This Is (and Isn’t)
This is not a replacement for full observability stacks. It’s a lightweight, in-app way to answer:
- Which endpoints are slow?
- How bad is the tail latency?
- Are errors creeping up?
If you need distributed tracing across 20 services, use the big tools. If you want answers in 30 seconds, use this.
What You Get
Metrics Dashboard
See every endpoint's request count, P50/P95/P99 latency, error rates, and throughput at a glance. Latency values are color-coded (green/yellow/red) so you can spot slow endpoints instantly.
Auto-Generated API Docs
Enable schema capture and API Observatory builds Swagger-like documentation from your live traffic — no manual spec writing needed. Endpoints are grouped by path prefix, path parameters are highlighted, and request/response schemas are inferred automatically.
Interactive Guide
A built-in reference explaining what each metric means, how to act on them, and API design best practices — right inside your dashboard.
Why API Observatory?
| | |
| --------------------- | ------------------------------------------------------------------------------ |
| Zero dependencies | No runtime dependencies. Nothing to audit, nothing to break. |
| Zero config | Works out of the box with sensible defaults. |
| Off the hot path | Metrics are recorded via setImmediate() — your response times stay the same. |
| Fixed memory | Circular buffer storage with automatic eviction. No memory leaks. |
| Three frameworks | Express, Fastify, and Koa adapters with identical feature sets. |
| Schema inference | Auto-generates API docs from live traffic — no OpenAPI spec required. |
Quick Start
npm install api-observatoryExpress
import express from "express";
import { expressObservatory } from "api-observatory";
const app = express();
app.use(expressObservatory());
app.use(express.json());
// ... your routes
app.listen(3000);Note: If you enable
auth(login-based authentication), mountexpress.json()andexpress.urlencoded()beforeexpressObservatory()so the login form body is parsed. See Login-Based Authentication.
Fastify
import Fastify from "fastify";
import { fastifyObservatory } from "api-observatory";
const app = Fastify();
app.register(fastifyObservatory);
// ... your routes
app.listen({ port: 3000 });Koa
import Koa from "koa";
import { koaObservatory } from "api-observatory";
const app = new Koa();
app.use(koaObservatory()); // Mount FIRST
// ... your routes
app.listen(3000);Then open http://localhost:3000/_observatory in your browser.
Schema Capture (API Docs)
Schema capture intercepts request and response bodies to build inferred JSON schemas from live traffic. Enable it to get the API Docs tab.
In code:
app.use(expressObservatory({ captureSchemas: true }));Or via environment variable:
OBSERVATORY_CAPTURE_SCHEMAS=trueHow it works
- Request bodies are captured from
req.body(requires a body parser) - Response bodies are captured by intercepting
res.json()/onSendhook /ctx.body - Schemas are inferred recursively from each observed body
- Repeated observations are merged — fields present in every request are marked
required, othersoptional - Type conflicts are widened (e.g.,
string | number)
All schema processing runs off the hot path via setImmediate(). It does not block responses. For high-throughput production APIs with large payloads, consider enabling it only in staging/development.
Important: Mount the middleware once. Multiple instances have isolated stores, so data will be split and the dashboard will only show partial data.
Configuration
All options are optional.
app.use(expressObservatory({
mountPath: '/_observatory', // Dashboard URL path
includePaths: ['/api/**'], // Only track these routes (glob)
excludePaths: ['/health'], // Skip these routes (glob)
retentionMs: 3_600_000, // 1 hour retention window
maxPerEndpoint: 10_000, // Circular buffer capacity
percentiles: [50, 95, 99], // Which percentiles to compute
htmlDashboard: true, // Serve HTML (false = JSON only)
captureSchemas: true, // Enable schema inference
apiKey: 'your-secret-key', // Dashboard authentication
redactFields: true, // Redact sensitive field names
rateLimit: { windowMs: 60000, maxRequests: 100 },
onRecord: (record) => { ... }, // Forward to external systems
onDashboardAccess: (event) => { ... }, // Audit logging
}));| Option | Type | Default | Description |
| ------------------- | --------------------- | ------------------ | -------------------------------------- |
| mountPath | string | /_observatory | Dashboard URL path |
| includePaths | string[] | [] | Glob patterns to track (empty = all) |
| excludePaths | string[] | [mountPath, ...] | Glob patterns to skip |
| retentionMs | number | 3_600_000 | Metrics retention window (ms) |
| maxPerEndpoint | number | 10_000 | Max records per endpoint |
| percentiles | number[] | [50, 95, 99] | Percentiles to compute |
| htmlDashboard | boolean | true | Serve HTML dashboard |
| captureSchemas | boolean | false | Enable schema capture |
| apiKey | string | — | API key for dashboard authentication |
| authenticate | function | — | Custom auth (headers) => boolean |
| redactFields | boolean \| string[] | false | Redact sensitive field names in schemas|
| rateLimit | object | — | { windowMs, maxRequests } for rate limiting |
| onRecord | function | — | Callback after each record |
| onDashboardAccess | function | — | Audit callback for dashboard access |
| auth | object | — | Login-based auth { users, verify, sessionSecret } |
| Environment Variable | Description |
| ----------------------------- | ----------------------------------------------------------- |
| OBSERVATORY_CAPTURE_SCHEMAS | Set to true to enable schema capture without code changes |
| OBSERVATORY_API_KEY | Set API key for dashboard authentication |
Security
By default, the dashboard is accessible to anyone with network access to your application. For production deployments, always enable security features.
Authentication
Why: Without authentication, anyone with network access can view your API metrics, inferred schemas, and reset your data. In production, this exposes sensitive information about your API structure and usage patterns.
API Key Authentication — The simplest way to protect the dashboard:
app.use(expressObservatory({
apiKey: process.env.OBSERVATORY_API_KEY
}));Access the dashboard with the X-Observatory-Key header:
curl -H "X-Observatory-Key: your-secret-key" http://localhost:3000/_observatory/metricsCustom Authentication — For JWT, session-based, or other complex auth:
app.use(expressObservatory({
authenticate: (headers) => {
const auth = headers['authorization'];
if (!auth || !auth.startsWith('Bearer ')) return false;
const token = auth.slice(7);
return isValidToken(token); // Your validation logic
}
}));Login-Based Authentication
For teams who need a proper login experience with username/password, use the auth option. Users are presented with a built-in login page:
Important (Express): When using
auth, mount body parsers before the observatory middleware so thatreq.bodyis available for the login form:const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(expressObservatory({ auth: { ... } }));
Config-based users — Define users directly in your config:
app.use(expressObservatory({
auth: {
users: [
{ username: 'alice', password: process.env.ALICE_PW },
{ username: 'bob', password: process.env.BOB_PW },
],
sessionSecret: process.env.SESSION_SECRET, // Required - for signing cookies
sessionMaxAge: 86400_000, // Optional - 24 hours default
}
}));Custom verify function — Integrate with your own database, LDAP, or SSO:
app.use(expressObservatory({
auth: {
verify: async (username, password) => {
const user = await db.users.findOne({ username });
if (!user) return null;
const valid = await bcrypt.compare(password, user.passwordHash);
return valid ? { username: user.username, email: user.email } : null;
},
sessionSecret: process.env.SESSION_SECRET,
}
}));How it works:
- Users visit
/_observatoryand are redirected to/_observatory/login - After successful login, a signed session cookie is set
- Users can access the dashboard until the session expires
- Logout is available at
/_observatory/logout
Login endpoints:
| Path | Method | Description |
|------|--------|-------------|
| /_observatory/login | GET | Login page |
| /_observatory/login | POST | Verify credentials |
| /_observatory/logout | GET | Clear session |
The auth option takes precedence over apiKey and authenticate if multiple are configured.
Field Name Redaction
Why: Schema capture infers field names from request/response bodies. Field names like password, ssn, credit_card, or api_key reveal sensitive data patterns even without exposing values. Attackers can use this information to understand your data model and target specific endpoints.
Solution: The redactFields option automatically redacts sensitive field names from schema output. Fields are replaced with { type: '[REDACTED]' }.
app.use(expressObservatory({
captureSchemas: true,
redactFields: true, // Use default sensitive fields list
}));Default redacted fields: password, passwd, secret, token, api_key, apikey, access_token, refresh_token, authorization, credential, private_key, ssn, credit_card, cvv, pin
Custom redaction list:
app.use(expressObservatory({
captureSchemas: true,
redactFields: ['password', 'ssn', 'secret', 'custom_sensitive_field'],
}));Rate Limiting
Why: Dashboard endpoints can be abused for reconnaissance or denial-of-service attacks. Without rate limiting, attackers can rapidly probe your API structure or overwhelm the metrics system.
Solution: The rateLimit option applies a sliding-window rate limit to all dashboard endpoints.
app.use(expressObservatory({
rateLimit: {
windowMs: 60_000, // 1 minute window
maxRequests: 100, // Max 100 requests per window
}
}));Rate limit is applied per API key (if authenticated) or per IP address. When exceeded, the dashboard returns 429 Too Many Requests with a retryAfterMs field.
Audit Logging
Why: In production, you need visibility into who accesses your metrics dashboard, when, and what they viewed. This is critical for security audits, compliance, and detecting unauthorized access.
Solution: The onDashboardAccess callback fires on every dashboard access attempt, successful or not.
app.use(expressObservatory({
onDashboardAccess: (event) => {
console.log({
type: event.type, // 'access_granted' | 'access_denied' | 'metrics_viewed' | 'schemas_viewed' | 'reset'
timestamp: event.timestamp,
endpoint: event.endpoint,
method: event.method,
ip: event.ip,
});
// Forward to your logging system (Datadog, Splunk, CloudWatch, etc.)
}
}));Event types:
access_granted— Authentication passedaccess_denied— Authentication failed or rate limitedmetrics_viewed— Metrics endpoint accessedschemas_viewed— Schema endpoint accessedreset— Metrics/schemas were resetlogin— User logged in successfullylogout— User logged out
Security Options Reference
| Option | Type | Default | Description |
| ------------------- | ----------------------- | ------- | --------------------------------------------------- |
| apiKey | string | — | Require X-Observatory-Key header with this value |
| authenticate | (headers) => boolean | — | Custom auth function |
| auth | object | — | Login-based auth { users, verify, sessionSecret } |
| redactFields | boolean \| string[] | false | Redact sensitive field names in schemas |
| rateLimit | object | — | Rate limit config { windowMs, maxRequests } |
| onDashboardAccess | (event) => void | — | Audit callback for all dashboard access |
Production Recommendations
// Recommended production config
app.use(expressObservatory({
// Authentication
apiKey: process.env.OBSERVATORY_API_KEY,
// Rate limiting
rateLimit: {
windowMs: 60_000,
maxRequests: 100,
},
// Schema security
captureSchemas: true, // Enable if needed
redactFields: true, // Redact sensitive fields
// Audit logging
onDashboardAccess: (event) => {
logger.info('Observatory access', event);
},
// Exclude sensitive routes
excludePaths: ['/auth/**', '/admin/**'],
}));Checklist:
- Enable authentication — Use
apiKey,authenticate, orauth(login-based) - Enable redaction — Use
redactFields: trueif using schema capture - Enable rate limiting — Prevent abuse with
rateLimit - Enable audit logging — Track access with
onDashboardAccess - Use HTTPS — Protect API keys and session cookies in transit
- Restrict network access — Firewall/VPC rules where possible
API Endpoints
| Method | Path | Description |
| ------ | --------------------------------- | -------------------------------------------------- |
| GET | /_observatory | HTML dashboard (or JSON if htmlDashboard: false) |
| GET | /_observatory/metrics | All endpoint metrics as JSON |
| GET | /_observatory/metrics/:method/* | Single endpoint metrics |
| GET | /_observatory/schemas | All captured schemas as JSON |
| GET | /_observatory/schemas/:method/* | Single endpoint schema |
| POST | /_observatory/reset | Clear all metrics and schemas |
Route Extraction
API Observatory automatically extracts parameterized route patterns (e.g., /v1/users/:id) instead of raw URLs. When route info is unavailable, it normalizes paths by replacing UUIDs, ObjectIds, and numeric IDs with :id.
| Framework | Source |
| --------- | ------------------------------ |
| Express | req.baseUrl + req.route.path |
| Fastify | request.routeOptions.url |
| Koa | ctx._matchedRoute |
Requirements
- Node.js >= 18
- Express >= 4, Fastify >= 4, or Koa >= 2 (peer dependencies, all optional)
License
MIT
