ASP.NET Core 10 APIs · Lesson 14

Activities, metrics and observable ownership

Instrument one bounded operation and inspect in-process listeners.

External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

Activities and measurements carry different evidence

An Activity represents an operation and can participate in a trace relationship, while a metric instrument records measurements for aggregation. A log message is another signal with its own model. The portable companion aggregates two known quote amounts, printing quote:2 and amount:25.00; it does not fabricate trace identifiers or call those prints an exporter. The external project uses real ActivitySource and Meter objects and installs local listeners explicitly. That creates a self-contained instrumentation experiment without claiming an OpenTelemetry collector, storage backend or distributed service topology has been configured.

Follow two operations through the local listeners

Each /quote request starts the custom quote activity, attaches a bounded category tag and adds one to the quote counter with an operation label. The using scope ends the activity as the handler returns. The ActivityListener records its stop, while the MeterListener observes the counter measurement. After two quote requests, /telemetry reports two activities and two measurements. Those counters deliberately listen only to this project's named source and instrument, excluding unrelated framework events. The measurements are observed in process; no HTTP telemetry export or collector acknowledgment occurs during the lab.

An instrument can exist without an active observer

ActivitySource can return null when no listener requests an activity. Code should therefore use the optional activity safely rather than assume tracing always allocates an operation object. This project installs a listener that requests the chosen data so the experiment is observable. Metrics likewise need listeners or an export pipeline to make their measurements visible outside the calling code. The source, meter and listeners have application-lifetime using declarations and are disposed after the host stops. A production integration should choose appropriate instrumentation ownership and collector configuration rather than retaining per-request listeners or creating a new meter for every operation.

Bound labels before expanding observability

The sample uses a fixed operation name and category instead of unbounded user identifiers. Metric dimensions can multiply the number of distinct series, so a convenient raw request value can create volume and privacy problems when promoted into a label. Activities can carry richer context, but they also need intentional data handling. Do not equate recording a value locally with a safe or useful production telemetry design. Likewise, a measurement count is not a count of successful purchases or persisted orders: this endpoint performs a small quote calculation and records only the operation it actually executes.

Practice: remove the observer independently of the operation

Run a separate project copy without the custom ActivityListener, keeping the null-safe activity call and the quote endpoint. Predict whether a quote should still succeed and distinguish that result from a stopped-activity observation. Keep the metric listener as a separate control, then test its removal independently. The hint is that instrumentation should not become an accidental functional dependency merely because the teaching listener makes it visible. Do not assert that no activity can exist anywhere in the process; other framework or externally configured listeners have their own sources and ownership.

Practice: add a bounded success dimension

Add a separate accepted-or-rejected outcome for a small controlled quote rule and choose a bounded metric dimension containing only those two names. Specify the expected measurements for one accepted and one rejected call before testing. Decide whether rejection should still create an activity and document that operation boundary. Avoid using the entire requested item name as a metric label. Then compare the domain counters with the listener observations: they should explain the same chosen events, while remaining different data structures with independent formatting and export responsibilities.

Distinguish instrumentation, observation and delivery

The real endpoint checks establish that this application emits custom activities and measurements that its local listeners observe. They do not establish distributed correlation across another service, collector delivery, retention or an alert rule. Preserve that claim when replacing the local probes with a production pipeline. The portable aggregation remains useful for the underlying business observations but cannot substitute for instrument tests. A clear observability design names the operation, instrument, dimensions, owner, observer and destination separately, making it possible to diagnose whether missing evidence comes from code that never recorded a signal or from a pipeline that never delivered it.

  • Install listeners deliberately; instrumentation calls do not prove an exporter exists.
  • Keep operation names and metric dimensions bounded and tied to the event actually measured.
External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Activities, metrics and observable ownership — real ASP.NET Core 10 project

Set up the SDK project

  1. Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
  2. From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
  3. Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
  4. Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
  5. GET /quote twice, then /telemetry. Inspect the bounded activity and metric names in Program.cs; no exporter service is configured.

Download complete SDK project ↗

Project files

WebLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

  </PropertyGroup>
</Project>
global.jsonDownload file ↗
{
  "sdk": {
    "version": "10.0.403",
    "rollForward": "latestPatch"
  }
}
Program.csDownload file ↗
using System.Diagnostics;
using System.Diagnostics.Metrics;
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); var probe = new TelemetryProbe();
using var source = new ActivitySource("SharpRune.WebLab"); using var meter = new Meter("SharpRune.WebLab"); var requests = meter.CreateCounter<long>("lab.quote.count");
using var activities = new ActivityListener { ShouldListenTo = s => s.Name == "SharpRune.WebLab", Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded, ActivityStopped = _ => probe.StopActivity() };
ActivitySource.AddActivityListener(activities);
using var measurements = new MeterListener();
measurements.InstrumentPublished = (instrument, listener) => { if (instrument.Meter.Name == "SharpRune.WebLab" && instrument.Name == "lab.quote.count") listener.EnableMeasurementEvents(instrument); };
measurements.SetMeasurementEventCallback<long>((instrument, value, tags, state) => probe.Measure(value)); measurements.Start();
app.MapGet("/quote", () => { using var activity = source.StartActivity("quote"); activity?.SetTag("lab.category", "catalog"); requests.Add(1, new KeyValuePair<string, object?>("operation", "quote")); return TypedResults.Ok(new { amount = 12.5m }); });
app.MapGet("/telemetry", () => new { activities = probe.Activities, measurements = probe.Measurements }); app.Run();
public sealed class TelemetryProbe { private int activities; private long measurements; public int Activities => Volatile.Read(ref activities); public long Measurements => Interlocked.Read(ref measurements); public void StopActivity() => Interlocked.Increment(ref activities); public void Measure(long value) => Interlocked.Add(ref measurements, value); }

Expected result

Two /quote requests followed by /telemetry show activities=2 and measurements=2 from the custom local listeners. No external collector or exporter is contacted.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var amounts = new[] { 12.5m, 12.5m }; Console.WriteLine("quote:" + amounts.Length); Console.WriteLine("amount:" + amounts.Sum().ToString("0.00", CultureInfo.InvariantCulture));
Open C# practice

C# practice output

quote:2
amount:25.00
Try this
An instrument can have no listener; a local listener is not a production exporter.

Further reading