Metrics API
Package: @onebun/metrics
Overview
OneBun provides Prometheus-compatible metrics with:
- Automatic HTTP request metrics
- System metrics (CPU, memory, event loop)
- Custom counters, gauges, and histograms
Enabling Metrics
In Application
import { OneBunApplication } from '@onebun/core';
import { AppModule } from './app.module';
const app = new OneBunApplication(AppModule, {
metrics: {
enabled: true,
path: '/metrics',
prefix: 'myapp_',
collectHttpMetrics: true,
collectSystemMetrics: true,
collectGcMetrics: true,
systemMetricsInterval: 5000,
defaultLabels: {
service: 'my-service',
environment: process.env.NODE_ENV || 'development',
},
},
});Configuration Options
interface MetricsOptions {
/** Enable/disable metrics (default: true) */
enabled?: boolean;
/** HTTP path for metrics endpoint (default: '/metrics') */
path?: string;
/** Default labels for all metrics */
defaultLabels?: Record<string, string>;
/** Enable HTTP request metrics (default: true) */
collectHttpMetrics?: boolean;
/** Enable system metrics (default: true) */
collectSystemMetrics?: boolean;
/** Enable GC metrics (default: true) */
collectGcMetrics?: boolean;
/** System metrics collection interval in ms (default: 5000) */
systemMetricsInterval?: number;
/** Metric name prefix (default: 'onebun_') */
prefix?: string;
/** HTTP request duration histogram buckets */
httpDurationBuckets?: number[];
}Built-in Metrics
HTTP Metrics
# Request duration histogram
http_request_duration_seconds_bucket{method="GET",route="/api/users",status_code="200",le="0.1"}
http_request_duration_seconds_sum{method="GET",route="/api/users",status_code="200"}
http_request_duration_seconds_count{method="GET",route="/api/users",status_code="200"}
# Request counter
http_requests_total{method="GET",route="/api/users",status_code="200"}System Metrics
# Process CPU
process_cpu_seconds_total
process_cpu_user_seconds_total
process_cpu_system_seconds_total
# Memory
process_memory_bytes{type="rss"}
process_memory_bytes{type="heapTotal"}
process_memory_bytes{type="heapUsed"}
process_memory_bytes{type="external"}
# Event loop lag
nodejs_eventloop_lag_seconds
# Active handles and requests
nodejs_active_handles_total
nodejs_active_requests_totalCustom Metrics
Accessing MetricsService
import { Service, BaseService } from '@onebun/core';
@Service()
export class OrderService extends BaseService {
async createOrder(data: OrderData): Promise<Order> {
// this.metrics is available in any BaseService or Controller
const counter = this.metrics?.createCounter({
name: 'orders_created_total',
help: 'Total number of orders created',
});
counter?.inc();
// ... process order
}
}Counter
Track cumulative values that only increase.
// Create counter - returns prom-client Counter object
const ordersCounter = this.metrics?.createCounter({
name: 'orders_created_total',
help: 'Total number of orders created',
labelNames: ['status', 'payment_method'],
});
// Increment using prom-client API
ordersCounter.inc({ status: 'completed', payment_method: 'credit_card' });
// Increment by value
ordersCounter.inc({ status: 'completed', payment_method: 'credit_card' }, 5);Gauge
Track values that can go up and down.
// Create gauge - returns prom-client Gauge object
const pendingGauge = this.metrics?.createGauge({
name: 'orders_pending',
help: 'Number of pending orders',
labelNames: ['priority'],
});
// Set value using prom-client API
pendingGauge.set({ priority: 'high' }, 42);
// Increment
pendingGauge.inc({ priority: 'high' });
// Decrement
pendingGauge.dec({ priority: 'high' });Histogram
Track distributions of values.
// Create histogram - returns prom-client Histogram object
const processingHistogram = this.metrics?.createHistogram({
name: 'order_processing_duration_seconds',
help: 'Order processing duration in seconds',
labelNames: ['order_type'],
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
// Observe value using prom-client API
const startTime = Date.now();
// ... process order ...
const duration = (Date.now() - startTime) / 1000;
processingHistogram.observe({ order_type: 'standard' }, duration);Decorator-based Metrics
The decorator creates its metric on first use, so applying one is enough — there is no createHistogram() to write first. It used to only look the name up and do nothing when it was absent, which meant a decorator copied from this page recorded nothing at all: the method ran, the scrape answered, and the series never appeared.
The name you pass is unprefixed; the scrape shows it prefixed
@Counted('emails_sent_total') with metrics: { prefix: 'myapp_' } appears in /metrics as myapp_emails_sent_total. Query the prefixed name in Prometheus — searching for the name you wrote is its own source of "my metric is missing".
Registering the metric yourself still wins, and is worth doing when you want real help text or histogram buckets chosen for your latencies — the decorator's generated ones are generic:
// In onModuleInit, before the first decorated call
this.metrics.createHistogram({
name: 'order_processing_duration_seconds',
help: 'How long an order takes end to end',
buckets: [0.05, 0.1, 0.5, 1, 5],
});@Timed()
Automatically time method execution.
import { Timed } from '@onebun/metrics';
@Service()
export class OrderService extends BaseService {
@Timed('order_processing_duration_seconds')
async processOrder(orderId: string): Promise<Order> {
// Method execution time is automatically recorded as a histogram
return this.doProcess(orderId);
}
}@Counted()
Automatically count method calls.
import { Counted } from '@onebun/metrics';
@Service()
export class EmailService extends BaseService {
@Counted('emails_sent_total')
async sendEmail(to: string, subject: string): Promise<void> {
// Counter incremented on each call
await this.smtp.send({ to, subject });
}
}@Gauged()
Update a gauge metric after method execution. The callback receives the class instance, so you can read its state. Supports both sync and async callbacks:
import { Gauged } from '@onebun/metrics';
@Service()
export class QueueService extends BaseService {
pendingItems: Item[] = [];
// Sync — read instance state directly
@Gauged('queue_depth', (self) => self.pendingItems.length)
async processNext(): Promise<void> {
const item = this.pendingItems.shift();
await this.handle(item);
}
// Async — query a database, call an API, etc.
@Gauged('total_items', async (self) => self.repo.count())
async addItem(item: Item): Promise<void> {
await this.repo.insert(item);
}
}Service Metrics Pattern
import type { Counter, Gauge, Histogram } from 'prom-client';
@Service()
export class PaymentService extends BaseService {
private paymentsCounter?: Counter<string>;
private processingHistogram?: Histogram<string>;
private queueGauge?: Gauge<string>;
constructor() {
super();
// Register custom metrics on service init
this.registerMetrics();
}
private registerMetrics(): void {
if (!this.metrics) return;
// Store references to metric objects for later use
this.paymentsCounter = this.metrics.createCounter({
name: 'payments_processed_total',
help: 'Total number of payments processed',
labelNames: ['status', 'method'],
});
this.processingHistogram = this.metrics.createHistogram({
name: 'payment_processing_seconds',
help: 'Payment processing duration',
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
this.queueGauge = this.metrics.createGauge({
name: 'payment_queue_size',
help: 'Current payment queue size',
});
}
async processPayment(payment: Payment): Promise<PaymentResult> {
const startTime = Date.now();
try {
const result = await this.gateway.charge(payment);
// Record success using prom-client API
this.paymentsCounter?.inc({ status: 'success', method: payment.method });
return result;
} catch (error) {
// Record failure
this.paymentsCounter?.inc({ status: 'failure', method: payment.method });
throw error;
} finally {
// Record duration
const duration = (Date.now() - startTime) / 1000;
this.processingHistogram?.observe(duration);
}
}
updateQueueSize(size: number): void {
this.queueGauge?.set(size);
}
}Metrics Endpoint
Access metrics at the configured path (default /metrics):
curl http://localhost:3000/metricsResponse (Prometheus format):
# HELP http_request_duration_seconds HTTP request duration in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{method="GET",route="/api/users",status_code="200",le="0.1"} 42
http_request_duration_seconds_bucket{method="GET",route="/api/users",status_code="200",le="0.5"} 87
http_request_duration_seconds_bucket{method="GET",route="/api/users",status_code="200",le="1"} 95
http_request_duration_seconds_sum{method="GET",route="/api/users",status_code="200"} 12.45
http_request_duration_seconds_count{method="GET",route="/api/users",status_code="200"} 95
# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/api/users",status_code="200"} 95
# HELP process_memory_bytes Process memory usage in bytes
# TYPE process_memory_bytes gauge
process_memory_bytes{type="rss"} 52428800
process_memory_bytes{type="heapTotal"} 20971520
process_memory_bytes{type="heapUsed"} 15728640Prometheus Integration
prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'onebun-app'
static_configs:
- targets: ['localhost:3000']
metrics_path: '/metrics'Grafana Dashboard
Common queries for Grafana:
# Request rate (requests per second)
rate(http_requests_total[5m])
# Request duration 95th percentile
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# Error rate
sum(rate(http_requests_total{status_code=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
# Memory usage
process_memory_bytes{type="heapUsed"}
# Custom metric
rate(payments_processed_total{status="success"}[5m])Complete Example
import { Module, Controller, BaseController, Service, BaseService, Get, Post, Body } from '@onebun/core';
import type { Counter, Gauge, Histogram } from 'prom-client';
// Service with custom metrics
@Service()
export class AnalyticsService extends BaseService {
private eventsCounter?: Counter<string>;
private processingHistogram?: Histogram<string>;
private queueGauge?: Gauge<string>;
constructor() {
super();
this.initMetrics();
}
private initMetrics(): void {
if (!this.metrics) return;
// Store references to prom-client metric objects
this.eventsCounter = this.metrics.createCounter({
name: 'analytics_events_total',
help: 'Total analytics events',
labelNames: ['event_type', 'source'],
});
this.processingHistogram = this.metrics.createHistogram({
name: 'analytics_processing_seconds',
help: 'Analytics event processing time',
buckets: [0.001, 0.005, 0.01, 0.05, 0.1],
});
this.queueGauge = this.metrics.createGauge({
name: 'analytics_queue_depth',
help: 'Current queue depth',
});
}
async trackEvent(eventType: string, source: string, data: unknown): Promise<void> {
const startTime = performance.now();
try {
// Process event
await this.processEvent(eventType, data);
// Record metrics using prom-client API
this.eventsCounter?.inc({ event_type: eventType, source });
} finally {
const duration = (performance.now() - startTime) / 1000;
this.processingHistogram?.observe(duration);
}
}
updateQueueDepth(depth: number): void {
this.queueGauge?.set(depth);
}
private async processEvent(eventType: string, data: unknown): Promise<void> {
// Event processing logic
this.logger.debug('Processing event', { eventType });
}
}
// Controller
@Controller('/analytics')
export class AnalyticsController extends BaseController {
constructor(private analyticsService: AnalyticsService) {
super();
}
@Post('/track')
async track(@Body() body: { event: string; source: string; data?: unknown }) {
await this.analyticsService.trackEvent(body.event, body.source, body.data);
return { tracked: true };
}
}
// Module
@Module({
controllers: [AnalyticsController],
providers: [AnalyticsService],
})
export class AnalyticsModule {}