ServiceLevelIndicators is a .NET library for emitting service-level latency metrics in milliseconds using the standard System.Diagnostics.Metrics and OpenTelemetry pipeline.
It is designed for teams that need more than generic request timing. The library helps measure meaningful operations, attach service-specific dimensions such as customer, location, operation name, and SLI outcome, and build SLO or SLA-oriented dashboards and alerts from those metrics.
Service level indicators (SLIs) are metrics used to track how a service is performing against expected reliability and responsiveness goals. Common examples include availability, response time, throughput, and error rate. This library focuses on latency SLIs so you can consistently measure operation duration across background work, ASP.NET Core APIs, and versioned endpoints.
Trellis.ServiceLevelIndicators emits operation latency metrics in milliseconds so service owners can monitor performance over time using dimensions that matter to their system. The metrics are emitted via the standard .NET Meter Class.
By default, a meter named Trellis.SLI with instrument name operation.duration is added to the service metrics. If you configure ServiceLevelIndicatorOptions.Meter, metrics are emitted from that meter instead. Metrics recorded with StartMeasuring(...) emit the following attributes.
- CustomerResourceId - The target resource of the operation — the noun in the URL path being read or modified, normalized to a stable identifier (tenant, subscription, account, work item). NOT the caller, NOT a per-request GUID, NOT a user ID or email. Example: for
GET /teams/{teamId}called by userxa1for teamteam1, the value is"team1", not"xa1". See the ASP.NET Core package README for the full mental model. - LocationId - The location where the service is running, such as public cloud in the West US 3 region. Azure Core
- Operation - The name of the operation.
- Outcome - The SLI outcome. Exact values are
Success,Failure,ClientError, andIgnored. Default success-rate queries should useSuccess / (Success + Failure).
Trellis.ServiceLevelIndicators.Asp adds the following dimensions.
- Operation - For ASP.NET endpoints, the operation name is the HTTP method plus the route template, resolved in this order: (1)
[ServiceLevelIndicator(Operation = "...")]attribute or.AddServiceLevelIndicator("op")override, (2) MVCAttributeRouteInfo.Template, (3) the endpoint'sRouteEndpoint.RoutePattern.RawText(Minimal APIs / conventional routing). Route placeholders such as{id}are preserved, never substituted with the concrete request value. If no bounded template is available, the middleware emits the sentinel"<METHOD> <unrouted>"and logs a warning — see that value in your metrics as a signal to add a route template. - Outcome - By default, 2xx and 3xx responses are
Success, common caller errors such as 400/401/403/404/409/412/422 areClientError, 429 and 5xx responses areFailure, and request-aborted cancellations areIgnored. - http.response.status.code - The http status code.
- http.request.method - The http request method (GET, POST, etc).
Difference between ServiceLevelIndicator and http.server.request.duration
| ServiceLevelIndicator | http.server.request.duration | |
|---|---|---|
| Resolution | milliseconds | seconds |
| Customer | CustomerResourceId | N/A |
| Error check | Outcome and HTTP status code |
HTTP status code |
This makes the library useful when generic HTTP server metrics are not enough, especially for multi-tenant services, APIs with customer-specific objectives, or workloads that need the same SLI model outside HTTP request handling.
Trellis.ServiceLevelIndicators.Asp.ApiVersioning adds the following dimensions.
- http.api.version - The resolved API version when used in conjunction with the API Versioning package. The value can be a version string,
Neutral,Unspecified, or an empty string for invalid or ambiguous requests.
-
Trellis.ServiceLevelIndicators
This library can be used to emit SLI for all .net core applications, where each operation is measured.
-
Trellis.ServiceLevelIndicators.Asp
For measuring SLI for ASP.NET Core applications use this library that will automatically measure each API operation.
-
Trellis.ServiceLevelIndicators.Asp.ApiVersioning
If API Versioning package is used, this library will add the API version as a metric dimension.
dotnet add package Trellis.ServiceLevelIndicatorsFor a concise package-selection and integration guide, see docs/usage-reference.md.
API references:
docs/api_reference/trellis-api-sli.md—Trellis.ServiceLevelIndicators(core)docs/api_reference/trellis-api-sli-asp.md—Trellis.ServiceLevelIndicators.Asp(middleware + attributes)docs/api_reference/trellis-api-sli-apiversioning.md—Trellis.ServiceLevelIndicators.Asp.ApiVersioning
For ASP.NET Core:
dotnet add package Trellis.ServiceLevelIndicators.AspFor API Versioning support:
dotnet add package Trellis.ServiceLevelIndicators.Asp.ApiVersioning-
Register SLI with open telemetry by calling
AddServiceLevelIndicatorInstrumentation.Example:
builder.Services.AddOpenTelemetry() .ConfigureResource(configureResource) .WithMetrics(builder => { builder.AddServiceLevelIndicatorInstrumentation(); builder.AddOtlpExporter(); });
If you configure
ServiceLevelIndicatorOptions.Meterwith a custom meter, register that same meter with OpenTelemetry:var sliMeter = new Meter("MyCompany.ServiceLevelIndicator"); builder.Services.AddOpenTelemetry() .ConfigureResource(configureResource) .WithMetrics(metrics => { metrics.AddServiceLevelIndicatorInstrumentation(sliMeter); metrics.AddOtlpExporter(); }); builder.Services.AddServiceLevelIndicator(options => { options.Meter = sliMeter; options.LocationId = ServiceLevelIndicator.CreateLocationId("public", AzureLocation.WestUS3.Name); });
-
Add ServiceLevelIndicator into the dependency injection.
AddMvc()is required for overrides present in MVC SLI attributes to take effect.Example:
builder.Services.AddServiceLevelIndicator(options => { options.LocationId = ServiceLevelIndicator.CreateLocationId("public", AzureLocation.WestUS3.Name); }) .AddMvc();
-
Add the middleware to the pipeline.
app.UseServiceLevelIndicator();
-
Register SLI with open telemetry by calling
AddServiceLevelIndicatorInstrumentation.Example:
builder.Services.AddOpenTelemetry() .ConfigureResource(configureResource) .WithMetrics(builder => { builder.AddServiceLevelIndicatorInstrumentation(); builder.AddOtlpExporter(); });
-
Add ServiceLevelIndicator into the dependency injection. By default, SLI is emitted for every routed endpoint when the middleware is present. Set
AutomaticallyEmitted = falseif you want Minimal APIs to opt in endpoint-by-endpoint with.AddServiceLevelIndicator().Example:
builder.Services.AddServiceLevelIndicator(options => { options.LocationId = ServiceLevelIndicator.CreateLocationId("public", AzureLocation.WestUS3.Name); });
-
Add the middleware to the ASP.NET Core pipeline.
Example:
app.UseServiceLevelIndicator();
-
Optional: when
AutomaticallyEmitted = false, addAddServiceLevelIndicator()to each route mapping that should emit SLI metrics.Example:
app.MapGet("/hello", () => "Hello World!") .AddServiceLevelIndicator();
You can measure a block of code by wrapping it in a using clause of MeasuredOperation.
Example:
void MeasureCodeBlock(ServiceLevelIndicator serviceLevelIndicator)
{
using var measuredOperation = serviceLevelIndicator.StartMeasuring("OperationName");
// Do Work.
measuredOperation.SetOutcome(SliOutcome.Success);
}Required tags must be stable and meaningful. The library bounds Operation for you via the route-template resolver and the <unrouted> sentinel; you are responsible for LocationId (set once from configuration) and CustomerResourceId (stable tenant / subscription / resource identifier). CustomerResourceId may be high-cardinality when the backend is designed for it, but it must not be a per-request generated value.
The same discipline applies to [Measure] parameters and any custom attributes added via AddAttribute(...). Avoid email addresses, request IDs, timestamps, or unconstrained free text unless your metrics backend is explicitly designed for high-cardinality telemetry.
ServiceLevelIndicator is a sealed IDisposable registered as a singleton; the DI container disposes it (and the Meter it created) at host shutdown — no manual cleanup needed. A Meter you supply via ServiceLevelIndicatorOptions.Meter is owned by you and is never disposed by SLI.
Once the Prerequisites are done, all controllers will emit SLI information.
The default operation name is the HTTP method plus the route template (placeholders such as {id} are preserved). The full resolution order is described under Trellis.ServiceLevelIndicators.Asp above.
-
To add API versioning as a dimension use package
Trellis.ServiceLevelIndicators.Asp.ApiVersioningand enrich the metrics withAddApiVersion.Example:
builder.Services.AddServiceLevelIndicator(options => { /// Options }) .AddMvc() .AddApiVersion();
-
http.request.methodis emitted by default by the ASP.NET Core middleware.AddHttpMethod()remains available as a no-op for older setup code. -
Enrich SLI with the
Enrichcallback. The callback receives aMeasuredOperationas context that can be used to setCustomerResourceIdor additional attributes. An async versionEnrichAsyncis also available.Example:
builder.Services.AddServiceLevelIndicator(options => { options.LocationId = ServiceLevelIndicator.CreateLocationId(Cloud, Region); }) .AddMvc() .Enrich(context => { // Pull a STABLE tenant/subscription identifier — NOT the caller's UPN/user ID. var tenantId = context.HttpContext.User.Claims .FirstOrDefault(c => c.Type == "tid")?.Value ?? "unknown"; context.SetCustomerResourceId(tenantId); // Caller identity belongs in a separate (still bounded) dimension, not in CustomerResourceId. var tier = context.HttpContext.User.Claims .FirstOrDefault(c => c.Type == "tier")?.Value ?? "free"; context.AddAttribute("CallerTier", tier); });
-
To override the default operation name, add the attribute
[ServiceLevelIndicator]and specify the operation name.Example:
[HttpGet("MyAction2")] [ServiceLevelIndicator(Operation = "MyNewOperationName")] public IEnumerable<WeatherForecast> GetOperation() => GetWeather();
-
To set the
CustomerResourceIdwithin an API method, mark the parameter with the attribute[CustomerResourceId][HttpGet("teams/{teamId}")] public IEnumerable<WeatherForecast> GetByTeam([CustomerResourceId] string teamId) => GetWeather();
Or use
GetMeasuredOperationextension method.[HttpGet("{customerResourceId}")] public IEnumerable<WeatherForecast> Get(string customerResourceId) { HttpContext.GetMeasuredOperation().CustomerResourceId = customerResourceId; return GetWeather(); }
-
To add custom Open Telemetry attributes.
HttpContext.GetMeasuredOperation().AddAttribute(attribute, value);
GetMeasuredOperation will throw if the route is not configured to emit SLI.
When used in a middleware or scenarios where a route may not be configured to emit SLI.
if (HttpContext.TryGetMeasuredOperation(out var measuredOperation)) measuredOperation.AddAttribute("CustomAttribute", value);
You can add additional dimensions to the SLI data by using the
Measureattribute. Parameters decorated with[Measure]are automatically added as metric attributes (dimensions) using the parameter name as the attribute key.[HttpGet("name/{first}/{surname}")] public IActionResult GetCustomerResourceId( [Measure] string first, [CustomerResourceId] string surname) => Ok(first + " " + surname);
-
To prevent automatically emitting SLI information on all controllers, set the option,
builder.Services.AddServiceLevelIndicator(options => { options.AutomaticallyEmitted = false; }) .AddMvc();
In this case, add the attribute
[ServiceLevelIndicator]on the controllers that should emit SLI.
Try out the sample weather forecast Web API.
For a local Grafana/Prometheus/OpenTelemetry Collector experience, run the provisioned dashboard in sample\Observability\Grafana. It shows SLI latency percentiles, success rate, failures, client errors, unknown customer diagnostics, and <unrouted> detection.
To view the metrics locally using the .NET Aspire Dashboard:
- Start the Aspire dashboard:
docker run --rm -it -d -p 18888:18888 -p 4317:18889 -e DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true -e DASHBOARD__OTLP__AUTHMODE=Unsecured --name aspire-dashboard mcr.microsoft.com/dotnet/aspire-dashboard:latest - Run the sample web API project and call the
GET WeatherForecastusing the Open API UI. - Open
http://localhost:18888to view the dashboard. You should see the SLI metrics under the instrumentoperation.durationwhereOperation = "GET WeatherForecast",Outcome = "Success",http.response.status.code = 200, andLocationId = "ms-loc://az/public/westus3".
- If you run the sample with API Versioning, you will see something similar to the following.

