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 | Trellis.ServiceLevelIndicators |
Core latency SLI measurement for console apps, workers, background jobs, and shared libraries |
| Automatically measure ASP.NET Core endpoints | Trellis.ServiceLevelIndicators.Asp |
Middleware, MVC, and Minimal API integration |
| Add API version as a metric dimension | Trellis.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 | Trellis.SLI by default |
| Instrument name | operation.duration |
| Unit | milliseconds (ms) |
| Required tag | CustomerResourceId |
| Required tag | LocationId |
| Required tag | Operation |
| Required tag | Outcome (Success, Failure, ClientError, or Ignored) |
For ASP.NET Core, the library also emits http.request.method and http.response.status.code; http.api.version is emitted when API version enrichment is enabled.
Install:
dotnet add package Trellis.ServiceLevelIndicatorsRegister with OpenTelemetry:
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddServiceLevelIndicatorInstrumentation();
metrics.AddOtlpExporter();
});Register the service:
builder.Services.AddServiceLevelIndicator(options =>
{
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3");
options.CustomerResourceId = "tenant-a";
});Measure work:
async Task ProcessOrder(ServiceLevelIndicator sli)
{
using var op = sli.StartMeasuring("ProcessOrder");
op.AddAttribute("OrderType", "Standard");
await Task.Delay(50);
op.SetOutcome(SliOutcome.Success);
}Direct recording is also available when you already know the elapsed time. Record(...) emits CustomerResourceId, LocationId, Operation, Outcome, and any custom attributes supplied to the call. Manual measurements default to Ignored unless you set an outcome.
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.AddServiceLevelIndicator(options =>
{
options.Meter = sliMeter;
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3");
options.CustomerResourceId = "tenant-a";
});Available registration overloads:
metrics.AddServiceLevelIndicatorInstrumentation();
metrics.AddServiceLevelIndicatorInstrumentation("MyCompany.ServiceLevelIndicator");
metrics.AddServiceLevelIndicatorInstrumentation(sliMeter);Install:
dotnet add package Trellis.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()
.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 Trellis.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. The value is the single resolved API version, Neutral, Unspecified, or an empty string when the requested version is invalid or ambiguous.
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 or use Measure(...) / MeasureAsync(...) helpers to infer it:
op.SetOutcome(SliOutcome.Success);For ASP.NET Core:
| Response outcome | Outcome |
|---|---|
2xx, 3xx |
Success |
400, 401, 403, 404, 409, 412, 422 |
ClientError |
429, 5xx |
Failure |
| Unhandled exceptions | Failure |
| Request-aborted cancellations | Ignored |
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. - Reusing reserved tag names such as
CustomerResourceId,LocationId,Operation,Outcome,activity.status.code,http.request.method, orhttp.response.status.codeas custom attributes.
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.ClassifyHttpOutcome(...)IServiceLevelIndicatorBuilder.Enrich(...)IServiceLevelIndicatorBuilder.EnrichAsync(...)EndpointConventionBuilder.AddServiceLevelIndicator(...)HttpContext.GetMeasuredOperation()HttpContext.TryGetMeasuredOperation(...)
API versioning package:
IServiceLevelIndicatorBuilder.AddApiVersion()