---
name: cratis-fundamentals-telemetry
description: Instrument a .NET service with Cratis.Metrics.Roslyn generated counters, gauges, histograms and spans, and subscribe/export them through Cratis.OpenTelemetry. Use for partial instrumentation declarations, missing telemetry, host setup or metric-name upgrades. Do not use for operating a collector or for application audit records.
license: MIT
---

<!-- Copyright (c) Cratis. All rights reserved. -->
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->

# Generated metrics and traces

Use this when a service needs measurements and spans, not business facts. Keep event sourcing and audit records separate from telemetry.

## Verified product sources

All source locators below are in **Cratis/Fundamentals v7.23.0**. Read the tag, not a sibling checkout's HEAD.

| Package | Version | Source |
| --- | --- | --- |
| `Cratis.Fundamentals` | `7.23.0` | `Source/DotNET/Fundamentals/Metrics/{Counter,Gauge,Histogram}Attribute.cs`, `Traces/SpanAttribute.cs`, `Diagnostics/TagAttribute.cs` |
| `Cratis.Metrics.Roslyn` | `7.23.0` | `Source/DotNET/Metrics.Roslyn/MetricsSourceGenerator.cs:42-130,174-238,285-337,354-406`; `Templates/Metrics.hbs:11-123` |
| `Cratis.OpenTelemetry` | `7.23.0` | `Source/DotNET/OpenTelemetry/OpenTelemetryExtensions.cs:29-180,215-315`; `InstrumentationExtensions.cs:17-31`; `CratisOpenTelemetryOptions.cs:19-34` |

Registration evidence: `Source/DotNET/Fundamentals/DependencyInjection/ServiceCollectionExtensions.cs:27-54`. Unkeyed names: `Metrics/UnkeyedMeter.cs:17-23`, `Traces/UnkeyedActivitySource.cs:17-23` under the same Fundamentals source root. Upgrade context: `Documentation/csharp/metrics/roslyn.md` (Exported Metric Names) and `Documentation/csharp/opentelemetry.md`.

## 1. Declare instrumentation beside its owner

Reference `Cratis.Fundamentals` for runtime types and `Cratis.Metrics.Roslyn` for generation. Use a **static partial class in a namespace**, containing **static partial methods**. The generator's candidate filter requires static classes even for metrics; a merely partial instance class is not enough (`MetricsSourceGenerator.cs:42-53,84-93`). Prefer extension declarations with short, unqualified attribute/type names, as below.

This declaration file is complete; it assumes the application's existing `ReportWorker` class in `MyApplication.Reports`:

```csharp
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using Cratis.Diagnostics;
using Cratis.Metrics;
using Cratis.Traces;

namespace MyApplication.Reports;

public static partial class ReportInstrumentation
{
    [Counter<int>("reports.completed", "Completed reports")]
    internal static partial void Completed(this IMeter<ReportWorker> meter,
        [Tag("reports.outcome")] string outcome);

    [Gauge<int>("reports.pending", "Pending reports")]
    internal static partial void Pending(this IMeter<ReportWorker> meter, int count);

    [Histogram<double>("reports.duration", "Report duration", "s")]
    internal static partial void Duration(this IMeter<ReportWorker> meter, double seconds);

    [Span("reports.build", ActivityKind.Internal)]
    internal static partial IActivityScope<ReportWorker> Build(
        this IActivitySource<ReportWorker> source,
        [Tag("reports.format")] string format);
}
```

- The first metric argument is `IMeter<T>` or `IMeterScope<T>`. A counter without a measurement increments by one. The first remaining parameter, in any position, whose type exactly matches the attribute's numeric type is the measurement, not a tag; avoid a second parameter of that type. Gauges and histograms require that measurement (`MetricsSourceGenerator.cs:285-337`).
- Remaining parameters are tags. Metric keys default to parameter names; span keys default to snake_case. `[Tag("key")]` from `Cratis.Diagnostics` overrides either (`:174-185,403`).
- `IMeterScope<T>` merges scope tags with method tags (`Templates/Metrics.hbs:34-38,67-71,96-100`). Do not put credentials, personal payloads or unbounded entity IDs in metric labels; choose bounded categories.
- Name, description and optional UCUM unit are distinct. A duration in seconds uses `"s"` and a measurement in seconds, not milliseconds (`HistogramAttribute.cs:14-29`, template `:109`).
- A span returns `IActivityScope<T>` and takes `IActivitySource<T>` first. Assign the returned scope with `using`; `CRT0001` checks disposal (`ActivityScopeUsingAnalyzer.cs:20`, `Templates/Metrics.hbs:118-122`).

Call-site excerpt inside the existing worker, with injected `IMeter<ReportWorker> meter` and `IActivitySource<ReportWorker> source`:

```csharp
using var activity = source.Build("pdf");
meter.Pending(3);
meter.Completed("succeeded");
meter.Duration(0.25);
```

This shows recording, not report execution. Both `AddBindingsByConvention()` and `AddSelfBindings()` register unkeyed `IMeter<>` and `IActivitySource<>` as singletons. For a plain host, use the normal type-discovery/convention setup from **cratis-fundamentals-type-discovery**; a generated method alone does not register its dependencies.

## 2. Subscribe the actual instrumentation name

**Unkeyed instruments use `typeof(T).FullName`**, not the metric name and not the host service name. `AddCratisInstrumentation()` subscribes only to `Cratis.*`. An application namespace such as `MyApplication.Reports.ReportWorker` needs an explicit meter/source subscription. This is the first check when generation works but nothing is exported.

Reference `Cratis.OpenTelemetry` explicitly for SDK/exporter setup. Host-setup excerpt for an existing `IHostApplicationBuilder builder` (including a `WebApplicationBuilder`), before building the host:

```csharp
using Cratis.OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using MyApplication.Reports;

builder.AddCratisOpenTelemetry(options =>
{
    options.ServiceName = "report-service";
    options.ConfigureMetrics = metrics => metrics.AddMeter(typeof(ReportWorker).FullName!);
    options.ConfigureTracing = tracing => tracing.AddSource(typeof(ReportWorker).FullName!);
});
```

`AddCratisOpenTelemetry` extends `IHostApplicationBuilder`. When the host already owns an OpenTelemetry builder, use `services.AddOpenTelemetry().WithCratis(configuration, configure)` instead; `WithCratis` extends `OpenTelemetryBuilder`. Do not add both setups. If the application only wants subscription helpers, `AddCratisInstrumentation()` extends both `TracerProviderBuilder` and `MeterProviderBuilder`, not the service collection (`OpenTelemetryExtensions.cs:29-71`, `InstrumentationExtensions.cs:24-31`).

Shared setup adds Cratis sources/meters, ASP.NET Core and HTTP client instrumentation, runtime metrics, and scoped/formatted logs. Application callbacks run before DI is built (`OpenTelemetryExtensions.cs:77-180`).

## 3. Configure export, without baking endpoints or secrets into code

At this tag, OTLP export is **opt-in**: no configured endpoint means no OTLP exporter. Environment values take precedence over configuration keys. Relevant standard keys (`OpenTelemetryExtensions.cs:215-315`):

| Key | Effect |
| --- | --- |
| `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES` | Resource values override application defaults |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Shared absolute HTTP/HTTPS endpoint |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `_METRICS_ENDPOINT`, `_LOGS_ENDPOINT` | Signal-specific endpoints override the shared endpoint |
| `OTEL_EXPORTER_OTLP_PROTOCOL` (or signal-specific equivalent) | `http/protobuf` selects HTTP; otherwise gRPC |
| `OTEL_TRACES_EXPORTER`, `OTEL_METRICS_EXPORTER`, `OTEL_LOGS_EXPORTER` | If supplied, must include `otlp` for that signal to export |
| `OTEL_SDK_DISABLED=true` | Disables providers |
| `OTEL_METRIC_EXPORT_INTERVAL`, `OTEL_METRIC_EXPORT_TIMEOUT` | Metric reader timing in milliseconds |
| `OTEL_TRACES_SAMPLER`, `OTEL_TRACES_SAMPLER_ARG` | Sampler and ratio configuration |

For HTTP/protobuf, the shared endpoint gains `/v1/traces`, `/v1/metrics` or `/v1/logs`; signal-specific endpoints are used as supplied. An enabled invalid endpoint throws `InvalidOpenTelemetryEndpoint`. Treat exporter headers as secrets; obtain them through the approved environment/secret mechanism, never a snippet or committed configuration.

## Upgrade and prove it

- Fundamentals stopped bringing the OpenTelemetry SDK transitively in **7.20.2**; add the explicit telemetry package when migrating a consumer that relied on that dependency (`Documentation/csharp/opentelemetry.md`).
- Regeneration after the **7.20.0** unit correction can rename exported Prometheus metrics: descriptions are now HELP text, not units. Rebuild the consumer and update dashboards/alerts that used the old description-suffixed names (`metrics/roslyn.md`, template `:48-50,80,109`).
- Build with analyzers and warnings as errors. Inspect generated implementations for correct measurements/tags and scope disposal. Do not edit generated files.
- Exercise one code path with a listener or the configured collector: observe the counter increment, histogram unit/measurement and completed span. A build or `skill-contains` assertion is not proof of export. Check namespace subscriptions, SDK disablement, sampler and endpoint independently before diagnosing the collector.
