# Opentelemetry Net Instrumentation

> Provides guidance for implementing OpenTelemetry instrumentation in .NET codebases, covering tracing (Activities/Spans), metrics, logs, naming conventions, error handling, performance, SDK setup, resources, context propagation, and API design best practices.

- Skill: `aaronontheweb/opentelemetry-net-instrumentation` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add aaronontheweb/opentelemetry-net-instrumentation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aaronontheweb/opentelemetry-net-instrumentation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: aaronontheweb (https://skillmd.com/u/aaronontheweb)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/aaronontheweb/opentelemetry-net-instrumentation

---


# OpenTelemetry .NET Instrumentation Skill

## When to Use

- Adding OpenTelemetry instrumentation to .NET code (traces, metrics, logs)
- Creating or modifying ActivitySources, Meters, or ILogger usage
- Setting up the OpenTelemetry SDK, resources, exporters, or sampling
- Reviewing telemetry implementations for spec compliance
- Optimizing instrumentation performance
- Designing telemetry APIs that become part of the public surface
- Implementing context propagation across service boundaries

## Architecture: .NET Is Different

**CRITICAL**: The .NET OpenTelemetry implementation is fundamentally different from other platforms. .NET provides tracing, metrics, and logging APIs **in the framework itself**.
That means **OTel does not provide a separate instrumentation API** — it uses the built-in .NET APIs and acts as the collection/export layer.

### The Three Built-in .NET APIs (Primary — Zero Dependencies)

| Signal | .NET Framework API | Namespace |
|--------|-------------------|-----------|
| **Tracing** | `ActivitySource` / `Activity` | `System.Diagnostics` |
| **Metrics** | `Meter` / `Counter<T>` / `Histogram<T>` / etc. | `System.Diagnostics.Metrics` |
| **Logging** | `ILogger<T>` | `Microsoft.Extensions.Logging` |

These are **the primary and only APIs** library authors should use for instrumentation.
They ship with the .NET runtime — **no NuGet packages required**.

### The OTel Collection/Export Layer (Secondary — Application Root Only)

OTel NuGet packages are the **collection and export layer**, added only at the application
composition root (not in libraries):

| Package | Purpose | When to add |
|---------|---------|-------------|
| `OpenTelemetry.Extensions.Hosting` | DI integration for ASP.NET Core / generic host | Application only |
| `OpenTelemetry.Exporter.Console` | Console exporter (dev/testing) | Application only |
| `OpenTelemetry.Exporter.OpenTelemetryProtocol` | OTLP exporter (production) | Application only |
| `OpenTelemetry.Exporter.Prometheus*` | Prometheus metrics endpoint | Application only |
| `OpenTelemetry.Instrumentation.AspNetCore` | Auto-instrument ASP.NET Core requests | Application only |
| `OpenTelemetry.Instrumentation.Http` | Auto-instrument HttpClient calls | Application only |
| `OpenTelemetry.Instrumentation.SqlClient` | Auto-instrument SQL calls | Application only |

### Package Decision Guide

**Before adding ANY OpenTelemetry NuGet package, discuss the trade-off with the user:**

> "You're about to add an OTel NuGet package. Is this an application where you need to
> export telemetry to an observability backend (Jaeger, Prometheus, OTLP collector)?
> If you're writing a library, you likely need **zero** OTel packages — just use
> `System.Diagnostics.ActivitySource` / `System.Diagnostics.Metrics.Meter` and let the
> consuming application configure the export pipeline. Do you want to proceed?"

**Library authors**: Add **nothing**. Use only `System.Diagnostics.*` and `ILogger`.
The consuming application wires up the SDK and exporters.

**Application authors**: Add `OpenTelemetry.Extensions.Hosting` + the exporters and instrumentation libraries you need. See [sdk-resources-and-logs-reference.md](sdk-resources-and-logs-reference.md) for full setup patterns.

**Never add** `OpenTelemetry.Api` to a library — `System.Diagnostics.*` IS the API.

For SDK setup, resource configuration, exporters, sampling, and logs integration, see [sdk-resources-and-logs-reference.md](sdk-resources-and-logs-reference.md).

## Core Principles

### Resiliency First
**CRITICAL**: Exceptions in diagnostic/tracing/metrics logic MUST NEVER impact application processing.
- Assume Activity instances can be null. Always protect against null Activity references except in Activity extension methods (use `activity?.ExtensionMethod()`)
- Guard all instrumentation code with appropriate null checks

### API Surface Awareness
- Any telemetry emitted becomes part of the public API surface
- Changes are subject to breaking changes guidelines
- Telemetry should be emitted by default (users opt-in to collection via OpenTelemetry extensions)
- Exception: High-cardinality metric dimensions may require explicit opt-in

### Standards Compliance
- Follow Microsoft best practices for [distributed tracing instrumentation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs)
- Follow [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/)
- Attribute values support: **string, boolean, double (IEEE 754), int64, byte arrays, and homogeneous arrays** of these primitive types. Null/empty values are valid and meaningful per the [OTel AnyValue spec](https://opentelemetry.io/docs/specs/otel/common/#anyvalue) — they MUST be stored and passed to exporters.
- Attribute **keys** must be non-null, non-empty strings

## Traces / Spans (Activities)

### ActivitySource Setup

```csharp
// ✅ CORRECT: Use ActivitySource, not DiagnosticSource
public class MyFeature
{
    // Primary ActivitySource - name typically matches the component or NuGet package name
    private static readonly ActivitySource ActivitySource = new("MyApp.MyComponent", "1.0.0");

    // Specialized ActivitySource for opt-in scenarios
    private static readonly ActivitySource DetailedActivitySource = new("MyApp.MyComponent.Detailed", "1.0.0");
}
```

**Rules**:
- Every component defines a primary `ActivitySource` for mainstream activities
- Name typically matches the component or NuGet package (e.g., `"MyCompany.MyLibrary"`)
- Version the ActivitySource using SemVer
- Create separate `ActivitySource`s for specialized or opt-in scenarios. Use hierarchical source names, e.g. `MyCompany.MyLibrary` and `MyCompany.MyLibrary.Detailed`, so consuming applications can subscribe only to the sources they want via `AddSource(...)` and backends can filter by instrumentation scope.

### Creating Activities

```csharp
// ✅ Check HasListeners, null-check, then guard expensive work behind IsAllDataRequested
if (ActivitySource.HasListeners())
{
    using var activity = ActivitySource.StartActivity("ProcessItem", ActivityKind.Internal);
    if (activity != null && activity.IsAllDataRequested)
    {
        activity.DisplayName = "Processing order #12345";
        activity.SetTag("app.item_id", itemId);
        activity.SetTag("app.item_type", itemType);
    }
}

// ❌ WRONG: Don't start activities in fire-and-forget tasks where the
// using scope ends before the async work completes (AsyncLocal context is lost)
async Task HelperAsync()
{
    using var activity = ActivitySource.StartActivity("Helper");
    _ = Task.Run(() => DoWorkAsync()); // ❌ activity disposed before task completes
}
```

**Rules**:
- Check `ActivitySource.HasListeners()` before creating (zero-allocation fast path)
- Always null-check Activity after creation (listener may filter or sample it out)
- Never start activities in async helper methods (`Activity.Current` uses `AsyncLocal`)
- Guard expensive tag computation behind `activity.IsAllDataRequested`
- Use W3C TraceContext. .NET Core 3.0+ / .NET 5+ uses it by default; older TFMs or .NET Framework apps may set `Activity.DefaultIdFormat = ActivityIdFormat.W3C` at startup and use `Activity.ForceDefaultIdFormat = true` to override hierarchical parents.

### Activity Naming

```csharp
// ✅ Unique operation name, friendly display name (null-check before accessing)
using var activity = ActivitySource.StartActivity(
    name: "ProcessItem",              // Unique, identifies class of spans
    kind: ActivityKind.Internal
);
if (activity != null)
    activity.DisplayName = "Processing order #12345"; // User-friendly, can be specific

// ❌ WRONG: Don't include runtime data in operation name
using var badActivity = ActivitySource.StartActivity($"Process_{itemId}"); // ❌
```

**Rules**:
- Each span type has unique `OperationName` (identifies statistically interesting class of spans)
- Operation name should NOT contain runtime data (only compile/config-time info)
- Use human-readable `DisplayName` for specifics
- Follow [OpenTelemetry span naming conventions](https://opentelemetry.io/docs/specs/otel/trace/api/#span)

### SpanKind Selection

Choose the correct `ActivityKind` to clarify the span's role in distributed tracing:

| `ActivityKind` | OTel SpanKind | When to use |
|----------------|---------------|-------------|
| `Internal` | `INTERNAL` | Default — in-process operations not crossing a remote boundary |
| `Server` | `SERVER` | Processing an incoming request/response call (HTTP server, gRPC server, RPC server) |
| `Client` | `CLIENT` | Making an outgoing request/response call (HTTP client, database client, RPC call) |
| `Producer` | `PRODUCER` | Enqueuing/publishing deferred work (message queue publish, event emit, job enqueue) |
| `Consumer` | `CONSUMER` | Dequeuing/processing deferred work (message queue receive, event handle, job dequeue) |

**Rules**:
- A single span SHOULD NOT serve more than one purpose
- Create the outgoing span **before** injecting its `SpanContext` into the request. If you inject first, the parent's context propagates instead and the outgoing span ends up dangling (no connection to the downstream call).
- See [traces-and-propagation-reference.md](traces-and-propagation-reference.md) for detailed SpanKind guidance with examples

### Span Attributes (Tags)

```csharp
// ✅ Application code: use your own namespace
activity?.SetTag("myapp.order_id", orderId);
activity?.SetTag("myapp.payment.status", "confirmed");

// ✅ Manual infrastructure instrumentation: use semantic conventions
// activity?.SetTag("db.system.name", "postgresql"); // custom database client
// activity?.SetTag("http.request.method", "GET"); // custom HTTP transport

// Values can be strings, numbers, booleans, or homogeneous arrays
activity?.SetTag("app.item_count", 42);
activity?.SetTag("app.related_ids", new int[] { 1, 2, 3 });

// ❌ WRONG: PascalCase, hyphen delimiter, plural, or unrelated namespace
activity?.SetTag("MyApp.OrderId", orderId);     // ❌ Wrong case
activity?.SetTag("myapp.order-id", orderId);    // ❌ Wrong delimiter
```

**Rules**:
- Namespace prefix matching your component: `myapp.*`, `myapp.db.*`
- All lowercase, underscore (`_`) delimiters, singular form
- Attribute values: string, boolean, double, int64, byte arrays, homogeneous arrays (null/empty valid per [AnyValue spec](https://opentelemetry.io/docs/specs/otel/common/#anyvalue))
- **Business/domain attributes**: use your own namespace (`myapp.*`).
- **HTTP, database, messaging, or RPC concepts you manually instrument**: use [semantic conventions](https://opentelemetry.io/docs/specs/semconv/). Do not duplicate attributes already emitted by auto-instrumentation. Do not use OTel namespaces as prefixes for custom attributes.

### Activity Status and Errors

```csharp
try
{
    await ProcessItemAsync(); // ✅ success: leave status Unset, do not call SetStatus(Ok) — see rules below
}
catch (Exception ex)
{
    if (activity != null)
    {
        activity.SetStatus(ActivityStatusCode.Error, ex.Message); // modern API
        activity.SetTag("error.type", ex.GetType().FullName);
    }
    throw;
}
```

**Rules** (per [Recording Errors](https://opentelemetry.io/docs/specs/semconv/general/recording-errors/) and the [Set Status API spec](https://opentelemetry.io/docs/specs/otel/trace/api/#set-status)):
- Leave span status **Unset** on success — do not call `SetStatus(ActivityStatusCode.Ok)`.
- `Ok` is for application code, not instrumentation libraries. The trace API spec: "Instrumentation Libraries SHOULD NOT set the status code to `Ok`, unless explicitly configured to do so (...) Application developers and Operators may set the status code to `Ok`" — typically to override a library-reported `Error` they've decided isn't a real failure (e.g. suppressing a noisy 404). Once set, `Ok` is final; later calls are ignored."
- On failure: **SHOULD** set `ActivityStatusCode.Error`, **SHOULD** set the `error.type` tag, **SHOULD** set the status description to the exception message.
- Use `SetStatus` — legacy `otel.status_code`/`otel.status_description` tags are no longer needed.
- Do **not** record errors that were retried or handled and let the operation complete gracefully — `Error` status and `error.type` describe operations that failed, not ones that recovered.

### Recording Exceptions — Keep Emitting Span Events, Add the Logs Opt-In

`exceptions-spans` — the convention behind `Activity.AddEvent(new ActivityEvent("exception", ...))` — carries a **Deprecated** status in favor of `exceptions-logs`, but the .NET ecosystem has not followed yet: no `OpenTelemetry.*` .NET package implements the spec's `OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN` opt-in (verified against `OpenTelemetry.Api` 1.17.0), and `OpenTelemetry.Instrumentation.AspNetCore` 1.17.0 itself still records exceptions via `Activity.AddException`, i.e. span events. That means the exporters, backends, and dashboards a typical .NET user has wired up today are built to read exception span events, not log-based exceptions. **Keep implementing span events as the default** — dropping them because the underlying spec document is marked Deprecated will silently break exception visibility for anyone still on today's tooling. Layer the newer logs-based path on top as an opt-in, exactly as the spec's own transition guidance describes for instrumentations moving off span events:

```csharp
// Mirrors the spec's OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN values: unset/anything else → spans only (today's default), "logs/dup" → both, "logs" → logs only.
private static readonly string? ExceptionSignalOptIn = Environment.GetEnvironmentVariable("OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN");
private static readonly bool EmitSpanEvents = ExceptionSignalOptIn != "logs";
private static readonly bool EmitLogs = ExceptionSignalOptIn is "logs" or "logs/dup";

try
{
    await ProcessItemAsync();
}
catch (Exception ex)
{
    activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
    activity?.SetTag("error.type", ex.GetType().FullName);

    if (EmitSpanEvents) // ✅ default — what current .NET tooling and dashboards actually consume today
    {
        activity?.AddEvent(new ActivityEvent("exception", tags: new ActivityTagsCollection
        {
            ["exception.type"] = ex.GetType().FullName,
            ["exception.message"] = ex.Message,
            ["exception.stacktrace"] = ex.ToString()
        }));
    }

    if (EmitLogs) // opt-in — the spec's forward direction; pass the exception instance while `activity` is still Activity.Current so the SDK derives trace_id/span_id and exception.type/message/stacktrace from it
    {
        logger.LogError(ex, "Item processing failed");
    }

    throw;
}
```

**Rules**:
- Don't set `exception.escaped` on the span event — it's deprecated outright: "no longer recommended to record exceptions that are handled and do not escape the scope of a span."
- Support `OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN` (`logs` / `logs/dup`) so consumers who are ready can opt into logs or dual-emission, but default to spans-only — this is the spec's own transition guidance, not an optional nicety, and it exists precisely so nobody has to choose one signal over the other before they're ready.
- When emitting logs (opt-in or dual), pick severity per `exceptions-logs`: `ERROR` for unhandled exceptions (especially on `SERVER`/`CONSUMER` spans), `WARN` for exceptions expected to be handled by the caller (especially on `CLIENT`/`PRODUCER` spans), `DEBUG` for exceptions that don't indicate an actual issue (e.g., a request cancelled client-side), `FATAL` only when the exception causes application shutdown.
- The log call **MUST** happen while the span it's associated with is still current — the spec requires "exception events emitted by instrumentations that also record spans for the same operation MUST be associated with the corresponding span context." Logging from a detached error-reporting callback after the `using` activity scope has ended silently correlates to the wrong span, or none. See [Accessing Activities](#accessing-activities) below.
- Only move to `logs`-only once you've held dual-emission for at least six months on a stable major version (the spec's own minimum) **and** confirmed your actual consumers read log-based exceptions — not on a fixed timeline alone.

See [traces-and-propagation-reference](traces-and-propagation-reference.md) for the full pattern, including how a library with no logging dependency in its core can still expose a callback so a separate OTel integration package can capture exception details while the right span is active.

### Accessing Activities

```csharp
var current = Activity.Current; // ❌ may be a user-created ambient span
using var ownedActivity = ActivitySource.StartActivity("MyOperation"); // ✅ captured reference
ownedActivity?.SetTag("myapp.key", value);
```
**Rules**: Do not rely on `Activity.Current` for spans you own; user code can replace it via `AsyncLocal`. Pass/store captured `Activity` only while alive. Store `ActivityContext` for propagation identity.

### Span Links

Links connect a span to other spans that are causally related but not in a direct parent-child relationship — batch processing, scatter/gather, trace boundary crossings.

```csharp
var links = new List<ActivityLink>
{
    new(activityContext1),
    new(activityContext2),
};

var activity = ActivitySource.StartActivity(
    ActivityKind.Internal, name: "batch-process", links: links);
```

See [traces-and-propagation-reference.md](traces-and-propagation-reference.md) for full link patterns including batch processing, scatter/gather, and trace boundary crossing.

### Context Propagation

Distributed tracing requires propagating trace context across process boundaries (HTTP calls, message queues, etc.) using W3C `traceparent` headers. In .NET, this is handled by `DistributedContextPropagator`. The OTel SDK configures W3C TraceContext propagation by default.

See [traces-and-propagation-reference.md](traces-and-propagation-reference.md) for propagation patterns, custom propagators, and manual inject/extract for non-standard transports.

## Metrics

### Meter and Metrics Class Setup

```csharp
public sealed class OrderProcessingMetrics : IDisposable
{
    private readonly Meter meter = new("MyApp.OrderProcessing", "1.0.0");
    private readonly Histogram<double> processingDuration =
        meter.CreateHistogram<double>("myapp.order.processing.duration", unit: "s");
    private readonly Counter<long> itemsProcessed =
        meter.CreateCounter<long>("myapp.order.processing.count", unit: "{order}");

    public void Dispose() => meter.Dispose();
}
```

**Naming Conventions** (follow [OTel semantic conventions](https://opentelemetry.io/docs/specs/semconv/general/metrics/)):
- Singular names, nested hierarchy: `myapp.order.processing.duration`
- Define units (s, ms, {item}, {connection}); avoid technical suffixes (`_counter`, `_histogram`)
- Start with pre-1.0.0 version until adoption proven

### Instrument Type Overview

.NET provides 7 metric instrument types. Choose the right one for your measurement:

| Instrument | .NET API | Behavior | Typical Use |
|------------|---------|----------|-------------|
| **Counter** | `CreateCounter<T>` | Monotonically increasing | Request counts, error counts |
| **UpDownCounter** | `CreateUpDownCounter<T>` | Increases or decreases | Queue size, active connections |
| **Histogram** | `CreateHistogram<T>` | Distribution of values | Durations, response sizes |
| **Gauge** | `CreateGauge<T>` (.NET 9+) | Synchronous instant value | Current measurement recording |
| **ObservableCounter** | `CreateObservableCounter<T>` | Async callback, monotonic | Periodically polled totals |
| **ObservableGauge** | `CreateObservableGauge<T>` | Async callback, non-monotonic | CPU/memory usage |
| **ObservableUpDownCounter** | `CreateObservableUpDownCounter<T>` | Async callback, bi-directional | Active tasks by priority |

See [metrics-and-instruments-reference.md](metrics-and-instruments-reference.md) for full creation/recording examples and observable callback patterns.

### Metric Recording Method Naming

```csharp
// ✅ Action/outcome-based naming, separate methods per outcome
public void OrderProcessingSucceeded(string orderType, TimeSpan duration) { /* Record */ }
public void OrderProcessingFailed(string orderType, Exception ex, TimeSpan duration) { /* Record */ }
public void ConnectionOpened() => connectionsOpen.Add(1);
public void ConnectionClosed() => connectionsOpen.Add(-1);

// ❌ WRONG: Name after metric, confusing signature
public void RecordOrderProcessingDuration(...) { } // ❌ don't name after metric
public void RecordError(bool succeeded, Exception? ex) { } // ❌ confusing signature
```

**Rules**:
- Name after action/outcome (`OrderProcessingSucceeded`), NOT after metric (`RecordXxx`)
- Separate methods per outcome (avoid boolean flags + optional exceptions)
- Event-based naming for state changes: `ConnectionOpened()`, `ItemQueued()`

### Metric Dimensions

```csharp
// ✅ Low-cardinality, predefined dimensions
processingDuration.Record(duration.TotalSeconds,
    new KeyValuePair<string, object?>("myapp.order_type", orderType),  // bounded set
    new KeyValuePair<string, object?>("outcome", "success"));         // bounded set

// ❌ High-cardinality: unbounded values cause cardinality explosion
failureCount.Add(1, new KeyValuePair<string, object?>("order_id", orderId)); // ❌ unbounded
```

**Rules**:
- Dimensions MUST be predefined and low-cardinality (item type, queue name, outcome)
- Avoid unbounded values (each unique value = new time series row → cardinality explosion)
- High-cardinality dimensions MUST be opt-in configuration
- Consistent names across components: `myapp.region` means the same everywhere
- Users can enable [exemplars](https://opentelemetry.io/docs/languages/dotnet/metrics/exemplars/) for trace correlation (not via dimensions)

## Performance Requirements

Instrumentation MUST be cheap by default. Follow these rules to minimize overhead:

### Zero-Allocation Fast Path

```csharp
// ✅ CORRECT: Guard with cheap checks
if (ActivitySource.HasListeners())
{
    using var activity = ActivitySource.StartActivity("Operation");
    // ... expensive work
}

// ✅ CORRECT: Use TagList (struct) for metrics
var tags = new TagList
{
    { "myapp.order_type", orderType },
    { "outcome", "success" }
};
counter.Add(1, tags);
```

### Timing

```csharp
// ✅ Timestamp math (no allocation)
var startTime = Stopwatch.GetTimestamp();
try { await ProcessAsync(); }
finally { var duration = Stopwatch.GetElapsedTime(startTime); metrics.OrderProcessingSucceeded(orderType, duration); }

// ❌ Allocates: Stopwatch.StartNew() or IDisposable timing wrappers
```

### Avoid Hidden Allocations

```csharp
// ❌ Allocates: string interpolation without IsAllDataRequested guard
activity?.SetTag("item", $"Processing {itemId}"); // ❌

// ✅ Guard expensive work behind IsAllDataRequested
if (activity?.IsAllDataRequested == true)
    activity.SetTag("item", $"Processing {itemId}");
```

**Rules**:
- No `Stopwatch.StartNew()` (use `Stopwatch.GetTimestamp()`/`GetElapsedTime`)
- Prefer `TagList` (struct) over arrays/dictionaries
- No LINQ, string interpolation, or async state machines in hot paths without guards

## Testing Requirements

### Span Tests

```csharp
[Test]
public async Task Should_create_processing_span_with_correct_parent()
{
    // Arrange
    using var parent = new Activity("Parent").Start();

    // Act
    await handler.Handle(item);

    // Assert
    var processingSpan = recordedActivities.Single(a => a.OperationName == "ProcessItem");
    Assert.AreEqual(parent.Id, processingSpan.ParentId);
    Assert.AreEqual("myapp.item_type", processingSpan.Tags.First().Key);
}

[Test]
public void Should_not_introduce_breaking_changes_to_span_names()
{
    // Ensures string values in span names are under test
    Assert.AreEqual("ProcessItem", MyFeature.SpanName);
}
```

**Rules**:
- Test which spans activities connect to
- Test string values (span names, tag names) to prevent breaking changes
- Remember: telemetry is part of public API

## Versioning

- Telemetry versioning decoupled from package version
- Use SemVer semantics
- Traces and Metrics use separate versions (evolve independently)
- Start with pre-1.0.0 version until adoption/usefulness proven

```csharp
private static readonly ActivitySource ActivitySource = new("MyApp.MyComponent", "0.9.0");
private readonly Meter meter = new("MyApp.MyComponent", "0.8.0");
```

## Logs

.NET logs integrate with OpenTelemetry through the built-in `ILogger` API. The OTel SDK provides `AddOpenTelemetry()` on the logging builder to collect, process, and export logs. Log records are automatically correlated with traces via `TraceId`/`SpanId`.

See [sdk-resources-and-logs-reference.md](sdk-resources-and-logs-reference.md) for full logs integration patterns including correlation, redaction, structured logging, and severity filtering.

## Reference Files

- [traces-and-propagation-reference.md](traces-and-propagation-reference.md): SpanKind deep dive with examples, Span Links (batch, scatter/gather, trace boundary), Context Propagation (W3C traceparent, DistributedContextPropagator, custom propagators), Baggage, and the full modern exception recording pattern.
- [metrics-and-instruments-reference.md](metrics-and-instruments-reference.md): All 7 metric instrument types with creation/recording code and when-to-use guidance, observable instrument callback patterns, dimensions deep dive, exemplars, aggregation defaults, and units.
- [sdk-resources-and-logs-reference.md](sdk-resources-and-logs-reference.md): SDK initialization (ASP.NET Core + Console), Resource configuration (ResourceBuilder, AddService, AddDetector, custom detectors), Exporters (OTLP, Console, Jaeger, Zipkin, Prometheus), Instrumentation Libraries, Sampling (built-in, custom, env vars, head/tail-based), Logs integration (ILogger, correlation, redaction, structured logging), and environment variable configuration.

## References

- [OTel Specification Overview](https://opentelemetry.io/docs/specs/otel/overview/)
- [OTel Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/)
- [OTel Common Spec (AnyValue)](https://opentelemetry.io/docs/specs/otel/common/)
- [OTel Tracing SDK Spec](https://opentelemetry.io/docs/specs/otel/trace/sdk/)
- [OTel Metrics SDK Spec](https://opentelemetry.io/docs/specs/otel/metrics/sdk/)
- [OTel Logs Spec](https://opentelemetry.io/docs/specs/otel/logs/)
- [OTel Resource Spec](https://opentelemetry.io/docs/specs/otel/resource/)
- [OTel Context Spec](https://opentelemetry.io/docs/specs/otel/context/)
- [.NET Observability with OpenTelemetry (MS Learn)](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel)
- [OTel .NET Manual Instrumentation](https://opentelemetry.io/docs/languages/dotnet/instrumentation/)
- [OTel .NET Metric Instruments](https://opentelemetry.io/docs/languages/dotnet/metrics/instruments/)
- [OTel .NET Sampling](https://opentelemetry.io/docs/languages/dotnet/sampling/)
- [OTel .NET Zero-Code Instrumentation](https://opentelemetry.io/docs/zero-code/net/)
