Metricy.Extract 0.4.1

dotnet tool install --global Metricy.Extract --version 0.4.1
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Metricy.Extract --version 0.4.1
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Metricy.Extract&version=0.4.1
                    
nuke :add-package Metricy.Extract --version 0.4.1
                    

dotnet-metricy-extract

NuGet NuGet License: MIT

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:

  1. 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.
  2. 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 — description and calculation fields
  • [MetricLabel] attributes — per-label descriptions matched by label name
  • prometheus-net Metrics.CreateCounter/Gauge/Histogram/Summary calls with:
    • Inline CounterConfiguration/GaugeConfiguration/HistogramConfiguration/SummaryConfiguration with LabelNames
    • String-literal name and help arguments
  • 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 .cs files 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.4.1 100 5/1/2026
0.4.0 103 4/20/2026