This document is a compact reference for choosing the correct package and wiring it into a .NET application.
| Scenario | Package | Purpose |
|---|---|---|
| Measure code in any .NET app | ServiceLevelIndicators |
Core latency SLI measurement for console apps, workers, background jobs, and shared libraries |
| Automatically measure ASP.NET Core endpoints | ServiceLevelIndicators.Asp |
Middleware, MVC, and Minimal API integration |
| Add API version as a metric dimension | ServiceLevelIndicators.Asp.ApiVersioning |
Adds http.api.version enrichment for apps using Asp.Versioning |
These values are part of the library contract and should be treated as stable unless you are intentionally making a breaking change.
| Metric element | Value |
|---|---|
| Meter name | ServiceLevelIndicator by default |
| Instrument name | operation.duration |
| Unit | milliseconds (ms) |
| Required tag | CustomerResourceId |
| Required tag | LocationId |
| Standard tag | Operation |
| Standard tag | activity.status.code |
For ASP.NET Core, the library also emits http.response.status.code and can optionally emit http.request.method and http.api.version.
Install:
dotnet add package ServiceLevelIndicatorsRegister with OpenTelemetry:
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddServiceLevelIndicatorInstrumentation();
metrics.AddOtlpExporter();
});Register the service:
builder.Services.Configure<ServiceLevelIndicatorOptions>(options =>
{
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3");
options.CustomerResourceId = "tenant-a";
});
builder.Services.AddSingleton<ServiceLevelIndicator>();Measure work:
async Task ProcessOrder(ServiceLevelIndicator sli)
{
using var op = sli.StartMeasuring("ProcessOrder");
op.AddAttribute("OrderType", "Standard");
await Task.Delay(50);
op.SetActivityStatusCode(ActivityStatusCode.Ok);
}Direct recording is also available when you already know the elapsed time:
sli.Record("ProcessOrder", elapsedTime: 42);If you provide a custom Meter in ServiceLevelIndicatorOptions, register that same meter with OpenTelemetry.
var sliMeter = new Meter("MyCompany.ServiceLevelIndicator");
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddServiceLevelIndicatorInstrumentation(sliMeter);
metrics.AddOtlpExporter();
});
builder.Services.Configure<ServiceLevelIndicatorOptions>(options =>
{
options.Meter = sliMeter;
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3");
options.CustomerResourceId = "tenant-a";
});
builder.Services.AddSingleton<ServiceLevelIndicator>();Available registration overloads:
metrics.AddServiceLevelIndicatorInstrumentation();
metrics.AddServiceLevelIndicatorInstrumentation("MyCompany.ServiceLevelIndicator");
metrics.AddServiceLevelIndicatorInstrumentation(sliMeter);Install:
dotnet add package ServiceLevelIndicators.AspRegister services:
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddServiceLevelIndicatorInstrumentation();
metrics.AddOtlpExporter();
});
builder.Services.AddServiceLevelIndicator(options =>
{
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3");
options.CustomerResourceId = "tenant-a";
})
.AddMvc();Add middleware:
app.UseServiceLevelIndicator();Common MVC customization points:
builder.Services.AddServiceLevelIndicator(options =>
{
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3");
})
.AddMvc()
.AddHttpMethod()
.Enrich(context =>
{
context.SetCustomerResourceId("tenant-a");
context.AddAttribute("ProductTier", "Premium");
});Action-level attributes:
[HttpGet("orders/{customerId}/{orderType}")]
[ServiceLevelIndicator(Operation = "GetOrder")]
public IActionResult Get(
[CustomerResourceId] string customerId,
[Measure(Name = "OrderType")] string orderType)
=> Ok();Register services and middleware:
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddServiceLevelIndicatorInstrumentation();
metrics.AddOtlpExporter();
});
builder.Services.AddServiceLevelIndicator(options =>
{
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3");
options.CustomerResourceId = "tenant-a";
options.AutomaticallyEmitted = false;
});
app.UseServiceLevelIndicator();Mark each endpoint that should emit SLI data:
app.MapGet("/orders/{customerId}/{orderType}",
([CustomerResourceId] string customerId, [Measure(Name = "OrderType")] string orderType) => Results.Ok())
.AddServiceLevelIndicator("GetOrder");Install:
dotnet add package ServiceLevelIndicators.Asp.ApiVersioningRegister enrichment:
builder.Services.AddServiceLevelIndicator(options =>
{
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3");
})
.AddMvc()
.AddApiVersion();This adds the http.api.version metric dimension when Asp.Versioning is present.
These APIs are useful inside controllers, middleware, and endpoint handlers:
var op = HttpContext.GetMeasuredOperation();
op.CustomerResourceId = "tenant-a";
op.AddAttribute("ProductTier", "Premium");
if (HttpContext.TryGetMeasuredOperation(out var measuredOperation))
{
measuredOperation.AddAttribute("FeatureFlag", "NewCheckout");
}Use GetMeasuredOperation() when the route is guaranteed to emit SLI metrics. Use TryGetMeasuredOperation() in shared middleware or filters.
For non-HTTP code, set the outcome explicitly:
op.SetActivityStatusCode(ActivityStatusCode.Ok);For ASP.NET Core:
| Response outcome | activity.status.code |
|---|---|
2xx |
Ok |
5xx |
Error |
| Other status codes | Unset |
| Unhandled exceptions | Error |
Use stable dimensions that support aggregation and alerting.
Good values:
- Tenant or subscription ID
- Region or cloud environment
- Product tier
- API version
- A bounded route category or operation type
Avoid values that can explode cardinality unless your backend is designed for them:
- Email addresses
- Request IDs
- Timestamps
- Arbitrary user input
- Random GUIDs per request
- Using a custom
Meterbut only registering the default meter name with OpenTelemetry. - Treating
CustomerResourceIdas a per-request unique ID instead of a stable service dimension. - Forgetting
AddMvc()when relying on MVC conventions and attribute-based overrides. - Forgetting
.AddServiceLevelIndicator()on Minimal API endpoints whenAutomaticallyEmittedisfalse. - Renaming
CustomerResourceIdorLocationIdeven though downstream systems depend on those exact names.
Core package:
AddServiceLevelIndicator(Action<ServiceLevelIndicatorOptions>)AddServiceLevelIndicatorInstrumentation()AddServiceLevelIndicatorInstrumentation(string meterName)AddServiceLevelIndicatorInstrumentation(Meter meter)ServiceLevelIndicator.StartMeasuring(...)ServiceLevelIndicator.Record(...)ServiceLevelIndicator.CreateLocationId(...)ServiceLevelIndicator.CreateCustomerResourceId(...)
ASP.NET Core package:
UseServiceLevelIndicator()IServiceLevelIndicatorBuilder.AddMvc()IServiceLevelIndicatorBuilder.AddHttpMethod()IServiceLevelIndicatorBuilder.Enrich(...)IServiceLevelIndicatorBuilder.EnrichAsync(...)EndpointConventionBuilder.AddServiceLevelIndicator(...)HttpContext.GetMeasuredOperation()HttpContext.TryGetMeasuredOperation(...)
API versioning package:
IServiceLevelIndicatorBuilder.AddApiVersion()