Logging
codewithmukesh/dotnet-claude-kit
Observability overview and glue for .NET 10: how the pieces fit together, plus the cross-cutting parts owned here — ASP.NET health check endpoints (/health), correlation IDs, and log-level strategy.
Provides guidance for implementing OpenTelemetry instrumentation in .NET codebases, covering tracing (Activities/Spans), metrics, logs, naming conventions, error handling, performance, SDK setup…
$ npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install Aaronontheweb/dotnet-skills opentelemetry-net-instrumentation --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/Aaronontheweb/dotnet-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/opentelementry-dotnet-instrumentation .claude/skills/opentelemetry-net-instrumentation && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "opentelemetry-net-instrumentation" agent skill from https://github.com/Aaronontheweb/dotnet-skills/tree/master/skills/opentelementry-dotnet-instrumentation into .claude/skills/opentelemetry-net-instrumentation/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "opentelemetry-net-instrumentation", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/Aaronontheweb/dotnet-skills/tree/master/skills/opentelementry-dotnet-instrumentationType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install Aaronontheweb/dotnet-skills opentelemetry-net-instrumentation --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Aaronontheweb/dotnet-skills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/opentelementry-dotnet-instrumentation .agents/skills/opentelemetry-net-instrumentation && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "opentelemetry-net-instrumentation" agent skill from https://github.com/Aaronontheweb/dotnet-skills/tree/master/skills/opentelementry-dotnet-instrumentation into .agents/skills/opentelemetry-net-instrumentation/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "opentelemetry-net-instrumentation", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install Aaronontheweb/dotnet-skills opentelemetry-net-instrumentation --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Aaronontheweb/dotnet-skills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/opentelementry-dotnet-instrumentation .cursor/skills/opentelemetry-net-instrumentation && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "opentelemetry-net-instrumentation" agent skill from https://github.com/Aaronontheweb/dotnet-skills/tree/master/skills/opentelementry-dotnet-instrumentation into .cursor/skills/opentelemetry-net-instrumentation/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "opentelemetry-net-instrumentation", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/Aaronontheweb/dotnet-skills.git --path skills/opentelementry-dotnet-instrumentation--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install Aaronontheweb/dotnet-skills opentelemetry-net-instrumentation --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Aaronontheweb/dotnet-skills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/opentelementry-dotnet-instrumentation .gemini/skills/opentelemetry-net-instrumentation && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "opentelemetry-net-instrumentation" agent skill from https://github.com/Aaronontheweb/dotnet-skills/tree/master/skills/opentelementry-dotnet-instrumentation into .gemini/skills/opentelemetry-net-instrumentation/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "opentelemetry-net-instrumentation", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install Aaronontheweb/dotnet-skills opentelemetry-net-instrumentationInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/Aaronontheweb/dotnet-skills.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/opentelementry-dotnet-instrumentation .github/skills/opentelemetry-net-instrumentation && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "opentelemetry-net-instrumentation" agent skill from https://github.com/Aaronontheweb/dotnet-skills/tree/master/skills/opentelementry-dotnet-instrumentation into .github/skills/opentelemetry-net-instrumentation/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "opentelemetry-net-instrumentation", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install Aaronontheweb/dotnet-skills opentelemetry-net-instrumentation --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Aaronontheweb/dotnet-skills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/opentelementry-dotnet-instrumentation .opencode/skills/opentelemetry-net-instrumentation && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "opentelemetry-net-instrumentation" agent skill from https://github.com/Aaronontheweb/dotnet-skills/tree/master/skills/opentelementry-dotnet-instrumentation into .opencode/skills/opentelemetry-net-instrumentation/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "opentelemetry-net-instrumentation", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
opentelemetry-net-instrumentationProvides guidance for implementing OpenTelemetry instrumentation in .NET codebases, covering tracing (Activities/Spans), metrics, logs, naming conventions, error handling, performance, SDK setup…
Opentelemetry Net Instrumentation is an agent skill from Aaronontheweb/dotnet-skills. 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.
Its SKILL.md is about 6.9k tokens, which your agent loads only when the skill is triggered. The skill folder holds 3 other files (for example `metrics-and-instruments-reference.md`, `sdk-resources-and-logs-reference.md` and `traces-and-propagation-reference.md`).
It sits in DevOps & Cloud, covering Observability. It works with OpenTelemetry, .NET and ASP.NET Core. The repository describes itself as: Claude Code skills and sub-agents for .NET Developers. The licence is MIT.
Read from SKILL.md and the folder at commit 46003af. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are csharp).
From the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
opentelemetry.iolearn.microsoft.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Opentelemetry Net Instrumentation loads about 6.9k tokens when it runs. Until then it costs about 73 tokens; SKILL.md has 2,031 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from Aaronontheweb/dotnet-skills at commit 46003af, republished under its MIT licence (© Aaronontheweb). 2,031 words, ~6,931 tokens.
.claude/skills/opentelemetry-net-instrumentation/SKILL.md (or your agent's skills folder). This skill also uses 3 other files; get the full folder from GitHub.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.
| 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.
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 |
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.Meterand 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 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.
CRITICAL: Exceptions in diagnostic/tracing/metrics logic MUST NEVER impact application processing.
activity?.ExtensionMethod())// ✅ 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:
ActivitySource for mainstream activities"MyCompany.MyLibrary")ActivitySources 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.// ✅ 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:
ActivitySource.HasListeners() before creating (zero-allocation fast path)Activity.Current uses AsyncLocal)activity.IsAllDataRequestedActivity.DefaultIdFormat = ActivityIdFormat.W3C at startup and use Activity.ForceDefaultIdFormat = true to override hierarchical parents.// ✅ 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:
OperationName (identifies statistically interesting class of spans)DisplayName for specificsChoose 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:
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).// ✅ 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 delimiterRules:
myapp.*, myapp.db.*_) delimiters, singular formmyapp.*).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 and the Set Status API spec):
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."ActivityStatusCode.Error, SHOULD set the error.type tag, SHOULD set the status description to the exception message.SetStatus — legacy otel.status_code/otel.status_description tags are no longer needed.Error status and error.type describe operations that failed, not ones that recovered.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:
// 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:
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."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.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.using activity scope has ended silently correlates to the wrong span, or none. See Accessing Activities below.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 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.
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.
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.
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 for full link patterns including batch processing, scatter/gather, and trace boundary crossing.
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 for propagation patterns, custom propagators, and manual inject/extract for non-standard transports.
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):
myapp.order.processing.duration_counter, _histogram).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 for full creation/recording examples and observable callback patterns.
// ✅ 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 signatureRules:
OrderProcessingSucceeded), NOT after metric (RecordXxx)ConnectionOpened(), ItemQueued()// ✅ 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)); // ❌ unboundedRules:
myapp.region means the same everywhereInstrumentation MUST be cheap by default. Follow these rules to minimize overhead:
// ✅ 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);// ✅ 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// ❌ 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:
Stopwatch.StartNew() (use Stopwatch.GetTimestamp()/GetElapsedTime)TagList (struct) over arrays/dictionaries[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:
private static readonly ActivitySource ActivitySource = new("MyApp.MyComponent", "0.9.0");
private readonly Meter meter = new("MyApp.MyComponent", "0.8.0");.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 for full logs integration patterns including correlation, redaction, structured logging, and severity filtering.
© Aaronontheweb, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 3 other files in skills/opentelementry-dotnet-instrumentation of Aaronontheweb/dotnet-skills.
Open the folder on GitHubat commit 46003af
Opentelemetry Net Instrumentation next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Opentelemetry Net Instrumentation this skillAaronontheweb/dotnet-skills | 1.2k | — | ~6.9k | Automated safety check: Pass | MIT | |
| Loggingcodewithmukesh/dotnet-claude-kit | 756 | — | ~1.6k | Automated safety check: Pass | MIT | |
| Otel Dotnetollygarden/opentelemetry-agent-skills | 106 | — | ~988 | Automated safety check: Pass | Apache-2.0 | |
| Otel Instrumentationatherio-danp/cde-dotnetcc | 109 | — | ~1.3k | Automated safety check: Notes | None | |
| Dotnet Devopsnovotnyllc/dotnet-artisan | 232 | — | ~1.1k | Automated safety check: Pass | MIT | |
| LoggingResgrid/Core | 229 | — | ~1.4k | Automated safety check: Pass | Apache-2.0 |
codewithmukesh/dotnet-claude-kit
Observability overview and glue for .NET 10: how the pieces fit together, plus the cross-cutting parts owned here — ASP.NET health check endpoints (/health), correlation IDs, and log-level strategy.
ollygarden/opentelemetry-agent-skills
OpenTelemetry in .NET — DI/builder SDK setup (OpenTelemetry.Extensions.Hosting, AddOpenTelemetry, UseOtlpExporter, OpenTelemetrySdk.Create), native .NET instrumentation APIs (ActivitySource…
atherio-danp/cde-dotnetcc
Add or change OpenTelemetry tracing, metrics, and structured logging in the .NET API (apps/api) — OTLP exporter setup, custom ActivitySource spans, IMeterFactory metrics, log/trace correlation.
novotnyllc/dotnet-artisan
Configures .NET CI/CD pipelines (GitHub Actions with setup-dotnet, NuGet cache, reusable workflows; Azure DevOps with DotNetCoreCLI, templates, multi-stage), containerization (multi-stage…
Resgrid/Core
Observability for .NET 10 applications. An agent skill from Resgrid/Core.
codewithmukesh/dotnet-claude-kit
OpenTelemetry observability for .NET 10 applications. An agent skill from codewithmukesh/dotnet-claude-kit.
Aaronontheweb/dotnet-skills
Guides making .NET libraries trimming-safe and Native-AOT compatible: the MSBuild properties, trimming attributes, warning codes and a playbook of safe patterns.
Aaronontheweb/dotnet-skills
Write and simplify C property-based, model-based, and executable specification tests with CsCheck.
Aaronontheweb/dotnet-skills
Shows how to build entity actors with Akka.Hosting so the same code runs in local unit tests and in a sharded cluster in production.
Aaronontheweb/dotnet-skills
Publish .NET services as container images with the built-in SDK tooling (dotnet publish /t:PublishContainer, Microsoft.NET.Build.Containers) - no Dockerfile required.
Aaronontheweb/dotnet-skills
Guidance for Akka.NET actor systems covering EventStream versus DistributedPubSub, supervision, Props versus DependencyResolver, work distribution and testable cluster code.
Aaronontheweb/dotnet-skills
Sets up Akka.Management and Cluster.Bootstrap so Akka.NET clusters form through service discovery on Kubernetes, Azure or config instead of static seed nodes.
Works with
Categories
Provides guidance for implementing OpenTelemetry instrumentation in .NET codebases, covering tracing (Activities/Spans), metrics, logs, naming conventions, error handling, performance, SDK setup…. Opentelemetry Net Instrumentation is an agent skill from Aaronontheweb/dotnet-skills.NET codebases, covering tracing (Activities/Spans), metrics, logs, naming conventions, error handling, performance, SDK setup, resources, context propagation, and API design best practices.
Opentelemetry Net Instrumentation fits situations like: tasks that involve Observability.
Run `npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a claude-code`. Or copy the skill folder (skills/opentelementry-dotnet-instrumentation in Aaronontheweb/dotnet-skills) into .claude/skills/opentelemetry-net-instrumentation in your project. Claude Code loads it when a task matches its description.
Run `npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a codex`. Or copy the skill folder (skills/opentelementry-dotnet-instrumentation in Aaronontheweb/dotnet-skills) into .agents/skills/opentelemetry-net-instrumentation in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add Aaronontheweb/dotnet-skills --skill opentelemetry-net-instrumentation -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/opentelemetry-net-instrumentation, .gemini/skills/opentelemetry-net-instrumentation, .github/skills/opentelemetry-net-instrumentation and .opencode/skills/opentelemetry-net-instrumentation in your project.
SKILL.md names no scripts, command-line tools or credentials: Opentelemetry Net Instrumentation is instructions for the agent only.
SKILL.md names 2 domains. As links in the text: opentelemetry.io and learn.microsoft.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Opentelemetry Net Instrumentation is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.9k tokens (SKILL.md is roughly 28k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Opentelemetry Net Instrumentation: Logging (codewithmukesh/dotnet-claude-kit, 756 stars), Otel Dotnet (ollygarden/opentelemetry-agent-skills, 106 stars), Otel Instrumentation (atherio-danp/cde-dotnetcc, 109 stars) and Dotnet Devops (novotnyllc/dotnet-artisan, 232 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
Aaronontheweb (a GitHub user) maintains it in Aaronontheweb/dotnet-skills, which has 1,206 GitHub stars. The repository holds 35 skills in this directory. The repository was last updated on October 10, 2026.
Source: Aaronontheweb/dotnet-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.