Skip to content

Metrics

import 'package:gisila/gisila.dart';

Turns on HTTP metrics and the Prometheus endpoint when set as AppConfig.metrics.

class MetricsConfig {
const MetricsConfig({
String path = '/metrics',
String? bearerToken,
bool Function(Request request)? allow,
SendPort? hub,
List<double> buckets = defaultLatencyBuckets,
});
}

When to use: whenever Prometheus (or a compatible agent) scrapes your service.

Field Default Description
path '/metrics' Endpoint path. Requests to it are not counted.
bearerToken null Allow requests sending Authorization: Bearer <token>. Must be at least 16 characters, otherwise ArgumentError.
allow null Custom access check; takes precedence over bearerToken.
hub null A MetricsHub.sendPort when several isolates serve one port.
buckets defaultLatencyBuckets Buckets of the request-duration histogram, in seconds.

Access: with allow, whoever it accepts; otherwise with bearerToken, requests that send it; otherwise only direct loopback connections without X-Forwarded-For, Forwarded or X-Real-IP (so a reverse proxy on the same host doesn’t expose it). Everyone else gets a 404.

Recorded metrics:

Metric Type Labels
http_server_requests_total counter method, route, status
http_server_request_duration_seconds histogram method, route
http_server_requests_in_flight gauge
process_resident_memory_bytes gauge
process_start_time_seconds gauge

route is the route template (/users/<id>), never the raw path. Unmatched requests are unmatched; handlers you mounted by hand without a routeId are other. Methods outside the standard seven are OTHER.

AppConfig(metrics: MetricsConfig(bearerToken: Platform.environment['METRICS_TOKEN']))
Middleware metricsMiddleware(MetricsConfig config, MetricsRegistry registry, {MetricsHubMember? hub})

The middleware behind AppConfig.metrics. Use it directly only in a custom Shelf pipeline.

Holds an app’s metrics. GisilaApp.metrics is one.

class MetricsRegistry {
MetricsRegistry();
Counter counter(String name, {required String help, List<String> labels = const []});
Gauge gauge(String name, {required String help, List<String> labels = const []});
Histogram histogram(String name,
{required String help, List<String> labels = const [], List<double> buckets = defaultLatencyBuckets});
Map<String, Object?> snapshot();
String render();
}

When to use: register business metrics on app.metrics so they are served next to the HTTP metrics. Calling counter/gauge/histogram again with the same name returns the same metric (a different type throws StateError). Invalid metric or label names throw ArgumentError.

Label values must come from a small fixed set (a template name, a status), never from user input: every combination is kept in memory forever.

Method Description
snapshot() Plain-data copy of every metric, sendable to another isolate.
render() Prometheus text format, plus process metrics.
final sent = app.metrics.counter('emails_sent_total', help: 'Emails sent.', labels: ['template']);
sent.inc({'template': 'welcome'});
final queueDepth = app.metrics.gauge('jobs_waiting', help: 'Jobs waiting.');
queueDepth.set(17);
final render = app.metrics.histogram('pdf_render_seconds', help: 'PDF render time.');
final pdf = await render.time(() => renderPdf(order));
class Counter {
void inc([Map<String, String> labels = const {}, num by = 1]);
double value([Map<String, String> labels = const {}]);
}

A value that only goes up. inc with a negative by throws ArgumentError. labels must name exactly the declared labels.

class Gauge {
void set(num value, [Map<String, String> labels = const {}]);
void inc([Map<String, String> labels = const {}, num by = 1]);
void dec([Map<String, String> labels = const {}, num by = 1]);
double value([Map<String, String> labels = const {}]);
}

A value that goes up and down (queue depth, open connections).

class Histogram {
final List<double> buckets;
void observe(num value, [Map<String, String> labels = const {}]);
Future<T> time<T>(Future<T> Function() body, [Map<String, String> labels = const {}]);
}

Distribution of observed values (latencies, sizes). time runs body and observes its duration in seconds, even when it throws.

const List<double> defaultLatencyBuckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];

Default histogram buckets, in seconds.

Merges metrics from every isolate serving the same port, so a scrape on any isolate reports the whole process.

class MetricsHub {
MetricsHub({Duration collectTimeout = const Duration(seconds: 2)});
static const int maxMisses = 3;
final Duration collectTimeout;
SendPort get sendPort;
int get memberCount;
void close();
}

When to use: when you start the app in several isolates with start(shared: true). Create the hub in the main isolate and pass sendPort to each isolate’s MetricsConfig(hub: ...). An isolate that misses maxMisses collections in a row is dropped.

Future<void> main() async {
final hub = MetricsHub();
for (var i = 1; i < Platform.numberOfProcessors; i++) {
await Isolate.spawn(serve, hub.sendPort);
}
await serve(hub.sendPort);
}
Future<void> serve(SendPort hub) async {
final app = GisilaApp(config: AppConfig(metrics: MetricsConfig(hub: hub)));
// register controllers ...
await app.start(port: 8080, shared: true);
}
class MetricsHubMember {
MetricsHubMember(SendPort hub, MetricsRegistry registry);
final SendPort hub;
final MetricsRegistry registry;
Future<Map<String, Object?>> scrape({Duration timeout = const Duration(seconds: 5)});
void close();
}

One isolate’s link to a hub. GisilaApp creates it for you when MetricsConfig.hub is set. scrape returns every isolate’s metrics merged, or this isolate’s alone if the hub doesn’t answer in time.

Map<String, Object?> mergeSnapshots(Iterable<Map<String, Object?>> snaps)

Sums snapshots from several isolates: metrics with the same name and labels are added. A metric whose type or buckets differ from the first one seen is dropped.

String renderSnapshot(Map<String, Object?> snapshot)

Renders a snapshot (one isolate’s or a merged one) in the Prometheus text format, followed by the process metrics.

See also: Deploy your app.