@fluojs/metrics
v3.0.3
Published
Prometheus metrics exposure with isolated registries and low-cardinality HTTP metric middleware for Fluo.
Maintainers
Readme
@fluojs/metrics
Prometheus metrics exposure for fluo applications, including framework-aware HTTP metrics and platform telemetry.
Table of Contents
Installation
pnpm add @fluojs/metricsRequirements
@fluojs/metrics requires Node.js >=24.0.0 <27 as its package-owned support contract.
When to Use
- when your app should expose a
/metricsendpoint for Prometheus-compatible scraping - when HTTP latency and request counts should be instrumented without hand-written middleware
- when application telemetry should stay aligned with fluo readiness and health state
Quick Start
This module registration fragment belongs in an existing HTTP app. Follow Application Bootstrap Protocol for the server entrypoint and decorator configuration. Protect /metrics separately at the deployment boundary as described below.
import { MetricsModule } from '@fluojs/metrics';
import { Module } from '@fluojs/core';
@Module({
imports: [MetricsModule.forRoot({ http: true })],
})
class AppModule {}MetricsModule.forRoot() exposes GET /metrics by default. Pass http: true (or an http options object) when you want the module to install HTTP request instrumentation middleware. When HTTP instrumentation is enabled, the module records request totals, error counts, and request duration. For production deployments, make the scrape endpoint boundary explicit: either disable it with path: false until a platform-level proxy is in place, or attach dedicated endpoint middleware.
The scrape endpoint returns the active prom-client registry output with that registry's Prometheus content type. MetricsModule.forRoot() creates an isolated registry for each application bootstrap; reusing the same dynamic module class for another bootstrap receives fresh isolated metrics state. Configure METRICS_REGISTRY at bootstrap only when framework metrics and application-defined metrics intentionally share one scrape surface. The former registry module option is removed: migrate each independent application by supplying its own bootstrap provider.
Public Responsibilities
| Surface | Responsibility | Boundary |
| --- | --- | --- |
| MetricsModule.forRoot(...) | Wires the Prometheus scrape endpoint, default metrics, optional HTTP instrumentation, platform telemetry, and registry ownership. | provider currently accepts only 'prometheus'; path: false disables the scrape route and route-scoped endpoint middleware. |
| MetricsService | Application-facing facade for custom Counter, Gauge, and Histogram metrics on the active registry, plus getRegistry() for deliberate advanced registry sharing. | MetricsService is non-global: inject it from a module that directly imports a MetricsModule.forRoot(...) registration or imports a module that re-exports MetricsService; unrelated sibling modules do not receive it automatically. Use collector helpers for business/application metrics. Use getRegistry() only when an integration must hand the active prom-client Registry to code that cannot receive METRICS_REGISTRY at bootstrap. |
| METRICS_REGISTRY | Bootstrap provider token for a shared prom-client Registry. | Supply it only through FluoFactory.create(..., { providers }); it is the sole shared-registry input. |
| Registry | prom-client's Registry constructor re-exported by @fluojs/metrics/integration for shared-registry setups. | It is the same Prometheus registry implementation; duplicate metric names still fail according to Prometheus semantics. |
| METER_PROVIDER / PrometheusMeterProvider / meter types | Low-level meter bridge for first-party package integrations that need a provider token or backend-neutral counter/gauge/histogram facade. | Application code usually does not need this token unless it is composing package-level integrations; the only bundled provider backend today is Prometheus. |
| middleware | Module-level middleware that participates in the module middleware chain after framework HTTP metrics and endpoint-scoped middleware. | It is not route-scoped; use endpointMiddleware when only the scrape route should be protected. |
| endpointMiddleware | Class-based @fluojs/http middleware constructors bound only to the configured scrape endpoint. | Ignored only when path: false; any string path, including '', remains an active endpoint path. Functions or global middleware declarations are outside this option's contract. |
Common Patterns
Normalize HTTP path labels
MetricsModule.forRoot({
http: {
pathLabelMode: 'template',
unknownPathLabel: 'UNKNOWN',
},
});pathLabelMode: 'raw' is an unsafe opt-in. You must pass allowUnsafeRawPathLabelMode: true only when you can prove the path space is bounded.
Custom path label normalization
MetricsModule.forRoot({
http: {
pathLabelNormalizer: ({ path }) => (path.startsWith('/api/v1') ? '/api/v1/:resource' : path),
},
});Configure HTTP duration histogram buckets
MetricsModule.forRoot({
http: {
durationHistogramBuckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
},
});durationHistogramBuckets replaces the built-in HTTP request duration histogram's
prom-client defaults. Values are measured in seconds and must fit the latency
range you intend to alert on. Each boundary must be finite and strictly increasing;
invalid configuration is rejected during setup.
Protect or disable the metrics endpoint
import { ForbiddenException, type MiddlewareContext, type Next } from '@fluojs/http';
class MetricsTokenMiddleware {
async handle(context: MiddlewareContext, next: Next): Promise<void> {
if (context.request.headers['x-metrics-token'] !== 'secret-token') {
throw new ForbiddenException('Metrics endpoint requires x-metrics-token.');
}
await next();
}
}
MetricsModule.forRoot({
endpointMiddleware: [MetricsTokenMiddleware],
});
MetricsModule.forRoot({
path: false,
});endpointMiddleware accepts class-based @fluojs/http middleware constructors and binds them only to the metrics scrape endpoint. Middleware functions or global middleware declarations are not the package contract for this option. middleware remains module-level middleware and runs as part of the module chain after endpoint-scoped middleware, while endpointMiddleware is skipped entirely when path: false disables the scrape route. When HTTP instrumentation is enabled, failures thrown by endpoint middleware are recorded in the built-in HTTP request and error collectors.
Create custom metrics once and reuse them
MetricsService.counter(...), gauge(...), and histogram(...) create Prometheus collectors on the active registry. MetricsService is non-global: the provider or controller that injects it must belong to a module that directly imports a MetricsModule.forRoot(...) registration or imports a module that re-exports MetricsService; unrelated sibling modules do not receive it automatically. Create each custom metric once during provider construction or application startup, then reuse the returned collector when business actions occur.
import { Inject } from '@fluojs/core';
import { MetricsService } from '@fluojs/metrics';
@Inject(MetricsService)
class OrdersService {
private readonly ordersCreated: ReturnType<MetricsService['counter']>;
constructor(metrics: MetricsService) {
this.ordersCreated = metrics.counter({
name: 'orders_created_total',
help: 'Total orders created',
});
}
recordOrderCreated(): void {
this.ordersCreated.inc();
}
}Calling MetricsService.counter(...) again with the same name recreates the collector and follows Prometheus' duplicate-name failure behavior. Store and reuse the collector instead of creating it inside each request or command handler.
MetricsService.getRegistry() returns the same active prom-client Registry used by the module scrape endpoint, built-in HTTP collectors, platform telemetry, and custom collectors created through the service. When you own the bootstrap, configure an explicit shared registry with the public METRICS_REGISTRY token. Use getRegistry() for advanced integrations that receive MetricsService through DI and need to register a third-party Prometheus collector on the already active registry.
Share one registry for framework and app metrics
If your application chooses to import raw collectors such as Counter or
Registry directly from prom-client, add it to your application's dependencies:
pnpm add prom-client@fluojs/metrics uses prom-client internally, but its dependency does not make
prom-client a supported transitive import for your application. The setup below
uses the Registry re-export from @fluojs/metrics/integration, so it does not require that
direct dependency.
import { Module } from '@fluojs/core';
import { METRICS_REGISTRY, MetricsModule } from '@fluojs/metrics';
import { Registry } from '@fluojs/metrics/integration';
import { FluoFactory } from '@fluojs/runtime';
const registry = new Registry();
@Module({
imports: [MetricsModule.forRoot({ http: true })],
})
class AppModule { }
const app = await FluoFactory.create(AppModule, {
providers: [{ provide: METRICS_REGISTRY, useValue: registry }],
});Registry is re-exported by @fluojs/metrics/integration, so this setup needs no direct prom-client dependency. Create application collectors through the MetricsService pattern above.
When multiple metrics module instances intentionally share the same registry, built-in HTTP metrics reuse the existing http_requests_total, http_errors_total, and http_request_duration_seconds collectors instead of registering duplicate framework metrics only when their framework ownership, label schema, and effective HTTP instrumentation configuration match. Compatibility includes pathLabelMode, the exact pathLabelNormalizer function reference, unknownPathLabel fallback semantics, and ordered durationHistogramBuckets values, so incompatible module instances fail fast instead of mixing different HTTP series policies into one collector set. Built-in platform telemetry gauges follow the same ownership rule: module-created fluo_component_ready, fluo_component_health, and fluo_metrics_registry_mode gauges are reused only when their framework ownership and label schema match. Platform telemetry state is tracked per reused registry, so a later scrape replaces stale module-owned component readiness and health series from an earlier module instance before metrics are returned. The registry scrape wrapper keeps using the latest active module registration and restores the Registry's original metrics() function after the last registration closes. Application-defined duplicate names still fail fast.
Duplicate metric names still fail fast
Prometheus metric names must stay unique inside a registry. Shared-registry mode keeps that behavior intact instead of silently shadowing metrics. If an application predefines a built-in HTTP collector or platform telemetry gauge name, MetricsModule.forRoot() rejects the collision instead of reusing an app-owned collector.
Runtime platform telemetry
The module emits fluo-specific gauges that mirror the platform shell and registered component state.
fluo_component_ready:1when a component is ready, otherwise0.fluo_component_health:1when a component is healthy, otherwise0.fluo_metrics_registry_mode: gauge value1withmode="isolated"whenMetricsModule.forRoot()creates its registry, ormode="shared"when bootstrap suppliesMETRICS_REGISTRY. The label reports the effective registry ownership configuration selected during bootstrap; it does not infer registry sharing at scrape time.
The platform snapshot is refreshed during each registry scrape, including advanced MetricsService.getRegistry().metrics() scrape paths, and you can attach environment labels up front.
MetricsModule.forRoot({
platformTelemetry: {
env: 'production',
instance: 'web-01',
},
});Runtime platform telemetry scrape contract
Platform telemetry refreshes fluo_component_ready and fluo_component_health on each active registry scrape by resolving PLATFORM_SHELL, whether the scrape flows through the built-in /metrics controller or an advanced custom scraper using MetricsService.getRegistry().
- If
PLATFORM_SHELLis not registered, the scrape still succeeds and omits the platform telemetry series. - If
PLATFORM_SHELLbecomes unavailable after the last successful scrape, stalefluo_component_readyandfluo_component_healthseries are removed before metrics are returned. - If resolving
PLATFORM_SHELLfails for any other reason, the scrape surfaces that failure instead of swallowing it.
Disable default process and Node metrics
defaultMetrics defaults to true, so MetricsModule.forRoot() registers Prometheus default process and Node.js collectors once per registry unless you opt out.
MetricsModule.forRoot({
defaultMetrics: false,
});Public API
MetricsModule.forRoot(options)METRICS_REGISTRY, the bootstrap-only token for an intentionally sharedRegistryMetricsService, includingcounter(...),gauge(...),histogram(...), andgetRegistry()METER_PROVIDERPrometheusMeterProvider- Meter abstraction types:
MeterProvider,MeterCounter,MeterGauge, andMeterHistogram HttpMetricsMiddlewareand HTTP path-label option types- Module options including
provider(currently only'prometheus'), module-levelmiddleware, and endpoint-scopedendpointMiddleware Registry, re-exported fromprom-clientby@fluojs/metrics/integration
Operational defaults
pathdefaults to'/metrics', any string path including''exposes a scrape endpoint, andpath: falsedisables the scrape endpoint entirely.- When no bootstrap
METRICS_REGISTRYprovider is supplied, each application bootstrap owns a fresh isolated registry,MetricsService, meter provider, and telemetry collector set. - Supply
METRICS_REGISTRYonly throughFluoFactory.create(..., { providers }); an unrelated module provider with the same token does not configure metrics ownership. - The scrape response uses the active registry's Prometheus content type and registry contents.
defaultMetricsdefaults totrue, anddefaultMetrics: falsedisables Prometheus default process and Node.js collectors for that registry.endpointMiddlewarebinds class-based route-scoped middleware only to the scrape endpoint; with HTTP instrumentation enabled, endpoint middleware failures are counted by the built-in HTTP collectors.- HTTP metrics are installed only when
http: trueor anhttpoptions object is provided, and then default to template-normalized path labels. http.durationHistogramBucketsreplaces the built-in HTTP request duration histogram buckets with explicit second-based boundaries.- Built-in HTTP collectors are reused when module instances share one registry only if they are framework-owned, have the expected label schema, and use matching HTTP instrumentation configuration; platform telemetry gauges are reused only if they are framework-owned and have the expected label schema; custom application metric name collisions keep Prometheus' duplicate-name failure behavior.
- Shared Registry telemetry refresh remains installed while any owning metrics module is active and restores the original
metrics()function after the last module closes. - Raw path labels require
allowUnsafeRawPathLabelMode: trueand should stay limited to bounded internal routes. - Platform telemetry is omitted only when
PLATFORM_SHELLis genuinely missing; other resolution failures fail the scrape. - Stale platform telemetry series are removed when
PLATFORM_SHELLbecomes unavailable after the last successful scrape or when a later module instance refreshes a reused registry with a different platform snapshot.
Compatible duplicate copies
Compatible same-realm @fluojs/metrics copies recognize framework-owned HTTP collectors and default collectors already registered on the exact Registry. They reuse that registry-bound ownership metadata without sharing independent registries, application telemetry, or capability detection; platform capability detection remains separate from this contract. A real Registry from a compatible physical prom-client copy is accepted at the bootstrap provider boundary only after its full public Registry surface is validated before any collector or telemetry mutation; incomplete lookalikes are rejected.
Related Packages
@fluojs/http: contributes the request lifecycle that HTTP metrics observe@fluojs/runtime: provides platform state used by runtime telemetry gauges@fluojs/terminus: commonly paired with metrics for ops visibility
Example Sources
examples/ops-metrics-terminus/src/app.tspackages/metrics/src/metrics-module.test.ts
