Nanov.OpenTelemetry.Summary 0.0.2

dotnet add package Nanov.OpenTelemetry.Summary --version 0.0.2
                    
NuGet\Install-Package Nanov.OpenTelemetry.Summary -Version 0.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Nanov.OpenTelemetry.Summary" Version="0.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nanov.OpenTelemetry.Summary" Version="0.0.2" />
                    
Directory.Packages.props
<PackageReference Include="Nanov.OpenTelemetry.Summary" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Nanov.OpenTelemetry.Summary --version 0.0.2
                    
#r "nuget: Nanov.OpenTelemetry.Summary, 0.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Nanov.OpenTelemetry.Summary@0.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Nanov.OpenTelemetry.Summary&version=0.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Nanov.OpenTelemetry.Summary&version=0.0.2
                    
Install as a Cake Tool

Nanov.OpenTelemetry.Summary

A Summary metric for OpenTelemetry .NET. Computes quantiles (p95, p99, etc.) client-side and reports them as gauges through the standard OTLP pipeline.

Why

OpenTelemetry .NET doesn't have a Summary metric type. The built-in Histogram works, but each bucket boundary generates a separate time series. With 15 buckets across many pods and short export intervals, that gets expensive fast on backends like Coralogix.

This library computes quantiles inside your app and emits them as simple gauges. Fewer series, same insight.

Install

dotnet add package Nanov.OpenTelemetry.Summary

Quick start

using System.Diagnostics.Metrics;
using Nanov.OpenTelemetry.Summary;

var meter = new Meter("MyApp");
var summary = meter.CreateSummary("http.request.duration", unit: "ms");

// Record a value
summary.Record(42.5);

// Record with tags
summary.Record(42.5, new TagList {
    { "endpoint", "/api/users" },
    { "method", "GET" }
});

That's it. The summary registers observable gauges on the meter, so any configured OTel exporter picks them up automatically.

What gets reported

By default, each summary emits:

Metric Type Description
{name} Gauge Quantile values, tagged with quantile="0.95", quantile="0.99"
{name}.count Counter Total number of observations (monotonic)
{name}.sum Counter Sum of all observed values

Max is available as an opt-in:

Metric Type Description
{name}.max Gauge Maximum observed value in the window

Configuration

var summary = meter.CreateSummary("http.request.duration", "ms",
    description: "HTTP request latency",
    configure: options => options
        .WithQuantiles(0.50, 0.95, 0.99)   // default: 0.95, 0.99
        .WithMax()                          // opt-in
        .WithoutSum()                       // disable sum counter
        .WithoutCount()                     // disable count counter
        .WithBufferCapacity(8192)           // default: 4096
        .WithReservoir(sampleSize: 2048, alpha: 0.015));

Timing operations

Two patterns, depending on whether you know the tags upfront.

When tags are known ahead of time, use using:

using (summary.Time(new TagList { { "endpoint", "/api/users" } }))
{
    await ProcessRequest();
}

When tags depend on the outcome (success/failure, status code), use the manual pattern:

var timer = summary.Time();
try
{
    await ProcessRequest();
    timer.Record(new TagList { { "status", "success" } });
}
catch
{
    timer.Record(new TagList { { "status", "failure" } });
    throw;
}

Duration() is an alias for Time() if you prefer that naming.

OTel pipeline setup

Works with any OTel exporter. Just register the meter:

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddMeter("MyApp")
        .AddOtlpExporter());

Grafana / Coralogix dashboards

Latency overview (p95 & p99)

# p99 by endpoint, worst pod in the fleet
max by (endpoint) (http_request_duration_ms{quantile="0.99"})

# p95 by endpoint
max by (endpoint) (http_request_duration_ms{quantile="0.95"})

Use these as two time series on a single panel. Set the Y-axis unit to milliseconds. This gives you the tail latency per endpoint across all pods.

Request rate

sum by (endpoint) (rate(http_request_duration_ms_count[5m]))

Shows requests per second per endpoint. Good as a stacked area chart to see traffic distribution.

Average latency

sum by (endpoint) (rate(http_request_duration_ms_sum[5m]))
/
sum by (endpoint) (rate(http_request_duration_ms_count[5m]))

Average latency per endpoint. Useful alongside percentiles to spot whether the tail is an outlier or a systemic shift.

SLO alerting

# Alert when p99 exceeds 500ms for 5 minutes
max by (service) (http_request_duration_ms{quantile="0.99"}) > 500

In Coralogix, create an alert with this query and a for: 5m duration. Use max by (service) to catch the worst pod — if any single pod breaches the SLO, you want to know.

Suggested dashboard layout

  1. Top row: p95 and p99 latency (line chart), request rate (stacked area)
  2. Middle row: average latency (line chart), error rate from a separate counter if available
  3. Bottom row: per-endpoint breakdown tables or heatmaps

Filter everything by service and endpoint variables at the top of the dashboard.

How it works

The hot path (Record) writes to a lock-free swap buffer. No locks, no allocations, no contention.

A background aggregation drains the buffer into an exponentially decaying reservoir (EDR) when the buffer reaches a high-water mark. The reservoir holds a fixed number of samples (default 1028), biased toward recent observations.

At export time, the OTel collection callback drains any remaining buffer entries, computes quantiles from the reservoir, and resets it for the next window.

Architecture

Performance

Benchmarked against AppMetrics and prometheus-net on .NET 9 (ARM64):

Single-threaded record (hot path)

Operation Time Allocated
Summary.Record (no tags) ~3 ns 0 B
Summary.Record (with tags) ~4 ns 0 B
prometheus-net Observe (no labels) ~30 ns 0 B
prometheus-net Observe (cached child) ~30 ns 0 B
prometheus-net WithLabels + Observe ~100 ns 0 B
AppMetrics Timer.Time (no tags) ~290 ns 976 B
AppMetrics Timer.Time (with tags) ~252 ns 856 B

Key takeaways

  • ~10x faster than prometheus-net, ~100x faster than AppMetrics on the hot path
  • Zero allocations — the swap buffer, reservoir, tag lookup, and even OTel Measurement creation are all pre-allocated or stack-based
  • Lock-free writes via atomic swap buffer — no contention under concurrent load
  • Quantile computation happens only at export time (every ~60s), not on every observation

License

MIT

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.2 200 4/15/2026