Resource Graph Metrics Bridge
Every Resource System node, exposed as a standard
System.Diagnostics.Metrics.Meterfor any OTel exporter to pick up.
Status: ๐ง Partial ยท Visibility: Public ยท Level: ๐ฃ Advanced ยท Category: Observability
๐ฏ What it solves
Typhon's Resource System already tracks memory, capacity, disk I/O, throughput, and duration for every significant engine resource (page cache, WAL ring, transaction pool, indexes, ...), but that data lives behind an engine-internal snapshot API, not a metrics endpoint. Hand-writing OTel instruments for 50โ300 resource nodes, and keeping them in sync as resources are added or removed, is busywork every host application would otherwise repeat.
โ๏ธ How it works (in brief)
ResourceMetricsExporter creates one Meter named "Typhon.Resources" and registers one observable
instrument per metric kind (Memory, Capacity, DiskIO, Throughput, Duration) โ not one instrument per resource.
Each observable callback reads the most recently captured ResourceSnapshot and yields one Measurement<T> per
node that reports that kind, tagged with a resource_path attribute (and metric_name for the named
Throughput/Duration metrics). ResourceMetricsService is a self-contained Timer that calls UpdateSnapshot()
on the configured interval โ that periodic snapshot is the only standing cost; the observable callbacks
themselves only run when something actually scrapes. Metric names are built by OTelMetricNameBuilder, which
converts a resource path and metric kind into a dotted, snake_cased OTel name (e.g.
typhon.resource.memory.allocated_bytes).
๐ป Usage
using Microsoft.Extensions.DependencyInjection;
using Typhon.Engine;
services.AddSingleton<IResourceGraph>(sp => resourceGraph);
services.AddTyphonObservabilityBridge(options =>
{
options.SnapshotInterval = TimeSpan.FromSeconds(5);
options.MetricNamePrefix = "typhon.resource"; // default
options.ExportDurationMetrics = false; // opt out of a metric kind
});
// Start the background snapshot loop (registration alone does not start it).
var bridgeService = serviceProvider.GetRequiredService<ResourceMetricsService>();
bridgeService.Start();
// Point an OTel MeterProvider at the bridge's meter (ASP.NET Core / OpenTelemetry.Extensions.Hosting):
services.AddOpenTelemetry().WithMetrics(builder => builder
.AddMeter(ResourceMetricsExporter.MeterName) // "Typhon.Resources"
.AddPrometheusExporter()); // or AddOtlpExporter(), AddConsoleExporter(), ...
| Metric kind | OTel name suffix | Example |
|---|---|---|
| Memory | memory.allocated_bytes, memory.peak_bytes |
typhon.resource.memory.allocated_bytes |
| Capacity | capacity.current, capacity.maximum, capacity.utilization |
typhon.resource.capacity.utilization |
| DiskIO | disk_io.read_ops, disk_io.write_ops, disk_io.read_bytes, disk_io.write_bytes |
typhon.resource.disk_io.write_bytes |
| Throughput | throughput.count (tagged metric_name) |
typhon.resource.throughput.count{metric_name="CacheHits"} |
| Duration | duration.last_us, duration.avg_us, duration.max_us (tagged metric_name) |
typhon.resource.duration.avg_us{metric_name="Checkpoint"} |
| Option | Default | Effect |
|---|---|---|
MetricNamePrefix |
"typhon.resource" |
Namespace prefix for every emitted metric name |
ExportMemoryMetrics / ExportCapacityMetrics / ExportDiskIOMetrics / ExportThroughputMetrics / ExportDurationMetrics |
true |
Per-kind opt-out โ skips registering that instrument entirely |
SnapshotInterval |
5 s | How often the underlying snapshot (read by every callback) refreshes |
โ ๏ธ Guarantees & limits
- One instrument per metric kind, not per resource โ cardinality comes from the
resource_path(andmetric_name) attribute, not from instrument count, so adding resources never requires new registration. - Observable callbacks read a cached snapshot; they never touch the resource graph directly, so an idle OTel pipeline (no exporter attached) costs nothing beyond the snapshot interval itself.
- Values are only as fresh as the last
UpdateSnapshot()โ driven byResourceMetricsService.SnapshotInterval(default 5 s), not by the OTel scrape interval. - A resource only appears in a given instrument's output if its
IMetricSource.ReadMetricsactually wrote that metric kind for the current snapshot โ nodes that don't report a kind are simply absent, not zero. ResourceMetricsExporterimplementsIDisposable; disposing it disposes theMeterand detaches every instrument โ do this once, at host shutdown.- Prometheus auto-converts dots to underscores, so the same dotted OTel names work unmodified for both OTLP/Grafana and Prometheus scraping.
๐งช Tests
- ObservabilityBridgeTests โ
OTelMetricNameBuildername/path normalization and snake_casing,ResourceMetricsExportersnapshot lifecycle, per-kind metric opt-out, andMeterListener-observed instrument registration
๐ Related
- Sibling: ECS Metrics Exporter โ the other OTel metrics exporter; covers ECS/archetype state instead of Resource Graph nodes
- Sibling: Observability Bridge (Resources to OTel/Health/Alerts) โ the same bridge, documented from the Resources category's side
- Related features: Resource-Aware Health Checks, Threshold-Based Resource Alerting โ read the same
ResourceMetricsExportersnapshot - Related feature: Metric Reporting (IMetricSource / IMetricWriter) โ how resources produce the data this exports
- Parent feature: OpenTelemetry Metrics Export