Verify observed metric cardinality in .NET tests and CI. Run your normal workload, observe the metric series it actually emits and how many distinct values each tag carried, and fail when an instrument identity exceeds an explicit observed-series or per-tag distinct-value budget - without a collector, exporter, or observability backend.
The package is KeelMatrix.MetricBudget.
dotnet add package KeelMatrix.MetricBudget --version 0.1.1The package works from a test project or a CI step and needs no collector, exporter, or backend. It targets
net8.0 and netstandard2.0.
using KeelMatrix.MetricBudget;
using KeelMatrix.MetricBudget.Assertions;
using MetricBudgetSession session = MetricBudgetSession.Start(
new MetricBudgetOptions()
.ForInstrument("My.Service", "http.client.request.duration", budget =>
{
budget.MaxObservedSeries = 50;
budget.Tag("server.address").MaxDistinctValues = 5;
}));
await ExerciseApplication();
MetricBudgetReport report = session.Complete();
report.AssertWithinBudget();A failing run names the instrument, the breached budget, the observed count, the configured limit, and the offending tag keys, and never prints tag values. Reports still retain application-supplied meter names, versions, instrument names, and tag keys; review those identifiers before sharing output outside its intended audience.
Per-tag results also expose whether their counts may be incomplete because of series-cap exhaustion, a rejected tag
set, tag-key-cap exhaustion, instrument admission loss, or the per-key value cap. Such counts are lower bounds and
MetricBudgetTagResult.IsWithinBudget is false whenever any of those loss paths could affect the key.
Focused budget and observation assertions fail closed when their target delivered no measurements or when any affected accounting is incomplete, including rejected physical instances, same-name identities, series, tag sets, tag keys, or tag values. The bounded name-only rejection index records overflow uncertainty: every identity admitted after that overflow is marked incomplete. A focused assertion does not pass when that loss could make the target incomplete or ambiguous; unrelated instruments' incomplete accounting does not invalidate a fully tracked target when the index did not overflow.
Rule results list only selected instrument identities that were retained and enabled. If an admission or selector
bound rejects part of a rule's selected population, those identities are omitted and the affected rule's
IsWithinBudget is false; the top-level outcome follows the documented precedence and may be incomplete, a proven
violation, or invalid rather than a false pass.
The package README contains the package introduction, installation, quick start, and important limitations. The maintained detail is in the dedicated guides below: budgets and lifecycle in the API and safety docs, privacy and telemetry in the privacy guide, and recurring failures in troubleshooting.
- the distinct observed series an instrument produced during the exercised workload;
- the distinct values each configured tag key produced;
- tag-set identity that is order-independent and deterministic, so the same combination never counts twice;
- instrument identity that includes unit, description, measurement type, meter tags, and instrument tags while
excluding
Meter.Scopefrom the stream key. Retained results expose a privacy-safeIdentityDiscriminatorso focused assertions can select same-name streams without exposing static metadata values. When a name-only focused assertion is ambiguous, pass one of the listed discriminator values to the overload that acceptsidentityDiscriminator; - static metadata admission through dedicated bounded options, with rejected dimensions reported in
Safety.StaticMetadataFailuresrather than being attributed to delivered-tag limits; - explicit outcomes for a budget breach, invalid configuration, an instrument that was never observed, and a session that observed no measurements at all.
A report describes the workload you ran. It never claims to prove the maximum cardinality production can produce, and it is not an observability-cost estimate.
src/KeelMatrix.MetricBudget/ the shipping library package (net8.0, netstandard2.0)
tests/KeelMatrix.MetricBudget.Tests/ behavior, edge, concurrency, safety, telemetry, integration, resource tests
tests/KeelMatrix.MetricBudget.PackageConsumer/ isolated package-consumer smoke test (consumes the built .nupkg)
samples/KeelMatrix.MetricBudget.Sample/ runnable sample
docs/ series identity, observed-vs-production, safety, privacy, troubleshooting
src/MetricBudget.Probe.*, tests/MetricBudget.Probe.* Phase 0 feasibility probe and its downlevel host
The MetricBudget.Probe.* projects are development evidence from the feasibility phase. They are not packable,
they are not part of the product API, and nothing in the shipping library depends on them. They also keep their own
MetricBudget.Probe.sln, which is the solution their evidence commands use.
KeelMatrix.MetricBudget.sln contains the library, its tests, and the probe projects. The sample and package-consumer
projects are intentionally outside that solution: each restores the package from a local feed, so each is run
explicitly after packing rather than as part of a normal solution build.
- docs/examples.md - canonical custom-meter and ASP.NET Core/OpenTelemetry-flavored consumer examples.
- docs/series-identity.md - the exact, deterministic series identity rule.
- docs/observed-vs-production-cardinality.md - what a passing run does and does not tell you.
- docs/safety-bounds.md - the bounded-memory contract and incomplete states.
- docs/troubleshooting.md - "instrument not observed", selector ambiguity, and other recurring problems.
- docs/privacy-and-telemetry.md - what reaches logs, reports, and telemetry.
- PRIVACY.md and SECURITY.md - privacy summary and vulnerability reporting.
- CONTRIBUTING.md - contributor setup and repository validation.
dotnet restore KeelMatrix.MetricBudget.sln
dotnet build KeelMatrix.MetricBudget.sln -c Release
dotnet test tests/KeelMatrix.MetricBudget.Tests/KeelMatrix.MetricBudget.Tests.csproj -c Release
The test project targets net8.0 and net472. The net472 run executes the library's netstandard2.0 asset
against System.Diagnostics.DiagnosticSource 8.0.1 on .NET Framework, which is the only way to exercise that
asset honestly.
Package validation, including archive inspection and clean package-backed smoke tests:
pwsh -NoProfile -File scripts/verify-package.ps1The package gate records the exact checked-out candidate commit and workflow/branch ref in both archives, so ordinary
feature and pull-request verification tests the candidate being proposed. Release publication adds a separate strict
provenance check that fetches origin/main, requires the checked-out HEAD to equal it, and passes that verified SHA
through a scoped workflow output. Both package-backed projects restore from artifacts/packages; neither references a
project in this repository. See
docs/DEV.md for the complete reproducible validation sequence.
MIT. See LICENSE.