Metricy.Extract
0.4.1
dotnet tool install --global Metricy.Extract --version 0.4.1
dotnet new tool-manifest
dotnet tool install --local Metricy.Extract --version 0.4.1
#tool dotnet:?package=Metricy.Extract&version=0.4.1
nuke :add-package Metricy.Extract --version 0.4.1
dotnet-metricy-extract
CLI tool + annotations library that extract Prometheus metric metadata (name, type, help, description, calculation, labels) from compiled .NET assemblies via static reflection — no application startup required. Companion to the Metricy registry: the extractor emits a JSON snapshot that CI posts to Metricy to keep the metric catalogue up to date.
Installation
# Install the global extraction tool
dotnet tool install -g Metricy.Extract
# Add the annotation attributes to your service project
dotnet add package Metricy.Annotations
Or add the package reference directly in your .csproj:
<PackageReference Include="Metricy.Annotations" Version="0.4.0" />
Usage
dotnet metricy-extract --assembly bin/Debug/net9.0/MyService.dll --output metrics.json
Why
In an era where agents write and maintain dozens of services in parallel, re-analyzing code on every task to answer "what metrics does service X expose, what do they mean, and how are they computed?" is wasteful — each agent run burns tokens rediscovering facts the code already captures. The scalable alternative is a metric registry (the Metricy catalogue) that agents and humans consult directly instead of re-scanning source trees.
For such a registry to stay trustworthy, every metric must be:
- Described next to the code — business meaning, calculation algorithm, and label semantics annotated on the same line as the
Metrics.CreateCounter(...)call, not authored ad-hoc in PR descriptions or a separate wiki. - Exported deterministically — the same code in produces the same JSON out, on every build, with no AI-inferred descriptions and no drift between what the registry shows and what the service actually declares.
dotnet-metricy-extract is that export step. Annotate metrics once with [MetricInfo] / [MetricLabel], run the tool in CI on every build, post the JSON snapshot to Metricy. The registry stays current without any agent re-scanning code.
Technical advantage
Standard reflection (Assembly.LoadFrom) runs static initializers — a real service tries to open databases, read environment variables, start background jobs. Without infrastructure, the process crashes before metadata can be read. dotnet-metricy-extract reads the DLL via MetadataLoadContext without executing any code, so extraction works:
- In CI/CD without any infrastructure.
- On developer machines without Docker or databases running.
- On any .NET 6 / 7 / 8 / 9 / 10 assembly (the tool itself targets .NET 10, the target assembly can be any version).
Quick Example
Service code:
using Metricy.Annotations;
using Prometheus;
public static class AppMetrics
{
[MetricInfo(
description: "Total incoming HTTP requests across all endpoints.",
calculation: "Incremented in RequestLoggingMiddleware.InvokeAsync() on each completed request.")]
[MetricLabel("method", "HTTP method: GET, POST, PUT, DELETE")]
[MetricLabel("status_code", "HTTP response status code")]
public static readonly Counter HttpRequests = Metrics.CreateCounter(
"http_requests_total",
"Total HTTP requests processed",
new CounterConfiguration { LabelNames = new[] { "method", "status_code" } });
}
Both attributes use positional constructors — [MetricInfo("desc", "calc")] (positional) and [MetricInfo(description: "desc", calculation: "calc")] (named-argument) are equivalent and both supported.
Extracted JSON (excerpt):
{
"schema_version": "1.0",
"project": "MyService",
"extracted_at": "2026-04-20T10:00:00Z",
"extractor": { "name": "dotnet-metricy-extract", "version": "0.4.0" },
"metrics": [
{
"name": "http_requests_total",
"type": "counter",
"help": "Total HTTP requests processed",
"description": "Total incoming HTTP requests across all endpoints.",
"calculation": "Incremented in RequestLoggingMiddleware.InvokeAsync() on each completed request.",
"labels": [
{ "name": "method", "description": "HTTP method: GET, POST, PUT, DELETE" },
{ "name": "status_code", "description": "HTTP response status code" }
],
"source_location": {
"file": "src/MyService/Metrics.cs",
"line": 12,
"class": "AppMetrics",
"member": "HttpRequests"
}
}
]
}
CLI Parameters
| Parameter | Required | Default | Description |
|---|---|---|---|
--assembly <path> |
yes* | — | Path to the compiled .NET assembly (.dll). *Not required when --list-rules is specified. |
--output <path> |
no | stdout | Output file path. When omitted, JSON is written to stdout. |
--source-root <dir> |
no | auto-detect | Project source root containing .cs files. Auto-detected by walking up from the DLL path to the first directory with a .csproj file. |
--project <name> |
no | assembly name | Project name written into the snapshot. Defaults to the assembly name without .dll. |
--repo-root <dir> |
no | auto-detect | Repository root for computing repo-relative paths in source_location.file. Auto-detected by walking up from the source root looking for a .git folder, .sln, or .slnx file. |
--validate |
no | off | Enable validation. Runs all 16 built-in rules. Exit code 1 if any Error-severity violations remain. |
--strict |
no | false |
Treat all warnings as errors. Promotes all Warning-severity rules to Error, unless explicitly overridden with --warn-rule. |
--skip-rule <id> |
no | — | Disable a validation rule by ID. Repeatable. Unknown IDs print a warning to stderr. |
--enable-rule <id> |
no | — | Enable an off-by-default rule by ID. Repeatable. Unknown IDs print a warning to stderr. |
--warn-rule <id> |
no | — | Demote a rule from Error to Warning severity. Repeatable. |
--error-rule <id> |
no | — | Promote a rule from Warning to Error severity. Repeatable. |
--min-description-length <N> |
no | 20 |
Global minimum length (characters) for description and calculation fields. Can be overridden per rule with --rule-min-length. |
--rule-min-length <id>:<N> |
no | — | Per-rule override for minimum length. Format: RULE-ID:N. Repeatable. Example: --rule-min-length metric.label-description-min-length:5 |
--validation-report <path> |
no | — | Write machine-readable validation report JSON to this file. A short human-readable summary of violations is always written to stderr when --validate is set and violations exist, regardless of this flag. Exit code 2 if the path cannot be written. |
--list-rules |
no | off | Print the full validation rule catalogue to stdout and exit 0. Does not perform extraction or validation. |
Validation
Running --validate checks the extracted snapshot against 16 built-in rules — 7 errors + 8 warnings always-on + 1 warning off-by-default.
| Severity | Count | Exit code | When to use |
|---|---|---|---|
| Error | 7 | 1 | Required metadata absent, duplicate names, type conflicts |
| Warning (on) | 8 | 0 | Prometheus naming conventions and description quality |
| Warning (off) | 1 | 0 (disabled) | Opt-in via --enable-rule metric.label-high-cardinality-hint |
Rule catalogue
Use dotnet metricy-extract --list-rules to see the canonical up-to-date list. The current 16 rules:
| Rule ID | Severity | Default | Description |
|---|---|---|---|
metric.calculation-required |
Error | on | Annotation calculation must be set |
metric.description-required |
Error | on | Annotation description must be set |
metric.duplicate-name |
Error | on | Same metric name must not appear more than once in the snapshot |
metric.help-required |
Error | on | Metric help text must be a non-empty string |
metric.label-description-required |
Error | on | Every declared label must have an annotation-provided description |
metric.name-required |
Error | on | Native-call name argument must be a string literal, not a variable |
metric.type-consistency |
Error | on | The same metric name must not be registered with two different types |
metric.calculation-min-length |
Warning | on | Calculation must be at least 20 characters (configurable) |
metric.counter-total-suffix |
Warning | on | Counter metric names must end with _total |
metric.deprecated-not-annotated |
Warning | on | Declaration marked [Obsolete] but annotation lacks a deprecated flag. Registered placeholder — currently yields no violations. Full implementation lands in a later release. |
metric.description-min-length |
Warning | on | Description must be at least 20 characters (configurable) |
metric.histogram-unit-suffix |
Warning | on | Histogram names must end with a unit suffix (_seconds, _bytes, _ratio, etc.) |
metric.label-description-min-length |
Warning | on | Label description must be at least 10 characters (configurable) |
metric.name-snake-case |
Warning | on | Metric name must be snake_case |
metric.non-literal-metadata |
Warning | on | Metric name or help is computed at runtime and could not be statically resolved |
metric.label-high-cardinality-hint |
Warning | off | Label name matches known high-cardinality patterns (user_id, email, ip, uuid, session_id) |
Typical CI usage
# Block on errors only (default)
dotnet metricy-extract --assembly bin/Debug/net9.0/MyService.dll --validate
# Strict: block on any violation including warnings
dotnet metricy-extract --assembly bin/Debug/net9.0/MyService.dll --validate --strict
# Skip a noisy rule
dotnet metricy-extract --assembly bin/Debug/net9.0/MyService.dll --validate \
--skip-rule metric.deprecated-not-annotated
# Enable high-cardinality check for strict orgs
dotnet metricy-extract --assembly bin/Debug/net9.0/MyService.dll --validate \
--enable-rule metric.label-high-cardinality-hint
# Per-rule minimum length overrides
dotnet metricy-extract --assembly bin/Debug/net9.0/MyService.dll --validate \
--min-description-length 30 \
--rule-min-length metric.label-description-min-length:5
# Write machine-readable JSON report for agent-driven autofix
dotnet metricy-extract --assembly bin/Debug/net9.0/MyService.dll --validate \
--validation-report report.json
# Browse the full rule catalogue
dotnet metricy-extract --list-rules
What It Extracts
[MetricInfo]attributes —descriptionandcalculationfields[MetricLabel]attributes — per-label descriptions matched by label name- prometheus-net
Metrics.CreateCounter/Gauge/Histogram/Summarycalls with:- Inline
CounterConfiguration/GaugeConfiguration/HistogramConfiguration/SummaryConfigurationwithLabelNames - String-literal name and help arguments
- Inline
- Legacy
// [Metric]comment blocks — recognized for services still using an earlier comment-block tool's output; each block emits a deprecation warning to stderr. Migrate to[MetricInfo]attributes. - Source locations (file + line + class + member, repo-root-relative with forward-slash separators) via Roslyn analysis of
.csfiles when a source root is available
Output JSON Format
Full snapshot example:
{
"schema_version": "1.0",
"project": "SampleService",
"extracted_at": "2026-04-20T10:00:00Z",
"extractor": {
"name": "dotnet-metricy-extract",
"version": "0.4.0"
},
"metrics": [
{
"name": "http_requests_total",
"type": "counter",
"help": "Total HTTP requests processed",
"description": "Total incoming HTTP requests across all endpoints.",
"calculation": "Incremented in RequestLoggingMiddleware.InvokeAsync() on each completed request.",
"labels": [
{
"name": "method",
"description": "HTTP method: GET, POST, PUT, DELETE"
},
{
"name": "status_code",
"description": "HTTP response status code"
}
],
"source_location": {
"file": "src/SampleService/Metrics.cs",
"line": 17,
"class": "AppMetrics",
"member": "HttpRequests"
}
},
{
"name": "http_request_duration_seconds",
"type": "histogram",
"help": "HTTP request duration in seconds",
"description": "Duration of HTTP request handling from middleware entry to response write.",
"calculation": "Observed in RequestLoggingMiddleware.InvokeAsync() after downstream pipeline returns.",
"labels": [
{
"name": "endpoint",
"description": "Route template of the matched endpoint"
}
],
"source_location": {
"file": "src/SampleService/Metrics.cs",
"line": 26,
"class": "AppMetrics",
"member": "HttpRequestDurationSeconds"
}
}
]
}
Top-level fields:
| Field | Type | Description |
|---|---|---|
schema_version |
string | Always "1.0". Consumers must treat unknown keys as additive (forward-compatible). |
project |
string | Service/project name from --project or the assembly file name. |
extracted_at |
string | UTC ISO 8601 extraction timestamp. |
extractor.name |
string | Always "dotnet-metricy-extract". |
extractor.version |
string | Tool version. |
metrics[] |
array | Metrics sorted alphabetically by name. |
metrics[].name |
string | Prometheus metric name from the native Create* call. |
metrics[].type |
string | counter, gauge, histogram, or summary. |
metrics[].help |
string | Prometheus help string from the native call. |
metrics[].description |
string|null | Business-level description from [MetricInfo]. |
metrics[].calculation |
string|null | Calculation description from [MetricInfo]. |
metrics[].labels[] |
array | Labels sorted alphabetically by name. |
metrics[].labels[].name |
string | Label name from LabelNames. |
metrics[].labels[].description |
string|null | Description from [MetricLabel]. |
metrics[].source_location.file |
string|null | Repo-relative path to the declaration (forward slashes). |
metrics[].source_location.line |
int|null | 1-based line number of the declaration. |
metrics[].source_location.class |
string|null | Simple name of the declaring class. |
metrics[].source_location.member |
string|null | Field or property identifier. |
Limitations
| Pattern | Limitation |
|---|---|
using static Prometheus.Metrics; + bare CreateCounter(...) |
Not recognized. Call-site must use the Metrics.CreateCounter(...) receiver explicitly. |
Metrics.CreateCounter(name, help, cfg) where cfg is a variable |
Label names cannot be resolved statically — pass the configuration object inline. |
| Name / help / label names computed at runtime | Static analysis reads only string literals; non-literal arguments produce a [warn] to stderr and the metric is included without the affected field. |
System.Diagnostics.Metrics (Meter.CreateCounter<T>) |
Not supported. Only prometheus-net Metrics.Create* factory methods are recognized. |
| App.Metrics library | Not supported. |
prometheus-net fluent builder / Metrics.DefaultRegistry patterns |
Not supported. Use the standard Metrics.CreateCounter(name, help, config) form. |
| Source locations for generated code | source_location is null when the declaring member cannot be found in the source tree (e.g. generated classes, missing source root). |
Dependencies
| Package | Version | Purpose |
|---|---|---|
System.Reflection.MetadataLoadContext |
10.0.5 | Load DLLs without executing code, read attributes and types |
Microsoft.CodeAnalysis.CSharp |
4.11.0 | Roslyn parsing of .cs files for source location resolution |
System.CommandLine |
2.0.5 | CLI argument parsing |
Does not depend on: ASP.NET Core, Swashbuckle, Entity Framework, prometheus-net, or any infrastructure packages.
Requirements
- .NET 10 SDK (to run the tool)
- The target assembly's build output directory with all reference DLLs (needed for type resolution)
Building
dotnet build
dotnet test
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
This package has no dependencies.