Timingz is an ASP.NET Core middleware implementation for recording and communicating backend server metrics as outlined in the Server Timing specification. These timings can be viewed using the browser's developer tools and accessed using the PerformanceServerTiming interface.
- Default configuration gives priority to security and functionality must be explicitly enabled to avoid exposing potentially sensitive information
- Features can be toggled at the individual request level using any information available in the
HttpContext
of the request:- Enable/disable writing of the
Server-Timing
header - Include metric descriptions in the header or exclude to reduce size
- Include only the total request duration or add custom metrics for more detail
- Enable/disable writing of the
- Different types of metrics are available to cater for a range of usage scenarios:
Manual
metrics can be manually started and stopped multiple timesDisposable
metrics can be wrapped in ausing
statement with the duration captured at the end of the statement blockMarker
metrics do not contain a duration but can indicate significant events like a cache miss occuringPrecalculated
metrics can record values captured from an existing timing mechanism
- Metrics can be validated to highlight logic errors that could lead to invalid and misleading timings being recorded
- In addition to being sent in the
Server-Timing
header, metrics captured during the request can also be sent to another service or metrics package after the request has been completed - Integration with the .NET Activity API and OpenTelemtry .NET allows existing
Activity
based metrics to be included in theServer-Timing
header and exported to OpenTelemetry exporters - The
Timing-Allow-Origin
header can be included with configurable domains - Header values are written using the ZString (Zero Allocation StringBuilder) library to minimise memory allocations
Install the Timingz
package from NuGet into your ASP.NET Core web application using your preferred mechanism.
PowerShell Command:
Install-Package Timingz
.NET Core CLI:
dotnet add package Timingz
Update ConfigureServices
in your Startup
class or host builder to include a call to the AddServerTiming
extension method on IServiceCollection
. This will add the required services to the DI container and make the IServerTiming
service available to your code. You can also register your own IServerTimingCallback
implementations here with the appropriate service lifetime.
// This call is mandatory.
services.AddServerTiming();
// This call is only required if you have callback services.
services.AddSingleton<IServerTimingCallback, SampleServerTimingCallback>();
Update Configure
in your Startup
class or host builder to include a call to the UseServerTiming
extension method on IApplicationBuilder
. This will add the middleware to the ASP.NET Core pipeline and should be added before UseEndpoints
if present. The UseServerTiming
method accepts a callback for you to configure the options. Some options are global and others are specific to individual requests.
// Add the middleware before UseEndpoints.
app.UseServerTiming(options =>
{
// Configure the per-request options.
options.WithRequestTimingOptions((httpContext, requestOptions) =>
{
// The use of a query string parameter is only for demonstration.
var query = httpContext.Request.Query;
// Enable/disable the Server-Timing header.
requestOptions.WriteHeader = query.ContainsKey("timing");
// Enable/disable the inclusion of descriptions in the header.
requestOptions.IncludeDescriptions = query.ContainsKey("desc");
// Enable/disable the inclusion of custom metric values.
requestOptions.IncludeCustomMetrics = query.ContainsKey("custom");
});
// Add any required domains for the Timing-Allow-Origin header.
options.TimingAllowOrigins = new[] {"https://example.com"};
// Enable/disable the invocation of callback services.
options.InvokeCallbackServices = true;
// Enable/disable the validation of metrics.
options.ValidateMetrics = env.IsDevelopment();
// Precision of duration values written to header (default 3).
options.DurationPrecision = 3;
});
The IServerTiming
service is added with a ServiceLifetime
of Scoped
resulting in a new instance being created for each HTTP request. This allows the same service instance to be injected into different services used during processing of the HTTP request. The IServerTiming
service should not be injected into services that that have a ServiceLifetime
of Singleton
to avoid a captive dependency.
The service is safe to use from multiple threads during a request but a metric for a given name can only be added once. An attempt to create a metric with a duplicate name will result in an exception being thrown.
The Marker
method is used to add a metric that has a name but does not include a duration. These metrics are used like tags that indicate something of importance such as a cache miss occuring.
serverTiming.Marker("miss", "Cache miss");
The Manaul
method returns a metric that can be manually started and stopped multiple times. This allows you to add up the total duration for an activity that is interspersed between other calls.
var databaseMetric = serverTiming.Manual("db", "Database queries");
databaseMetric.Start();
// The first database query.
databaseMetric.Stop();
// Some other code.
databaseMetric.Start();
// The second database query.
databaseMetric.Stop();
The Disposable
method returns a metric that is already started and will have its duration recorded at the end of a using
statement block. This is useful when the code being measured occurs with in a single section of code.
using (serverTiming.Disposable("bus", "Send notification to bus"))
{
// Send notification to bus.
}
The Precalculated
method is used to record a metric that has been captured using a separate mechansim such as another metrics package. This allows you to continue using an existing metrics package and have its values included in the Server-Timing
header.
var duration = GetTimingFromOtherPackage();
serverTiming.Precalculated("external", duration, "External metric timing");
This method returns a list of the recorded metrics and can be used for debugging purposes.
It should be noted that if the total request duration metric is enabled this will not be present until the HTTP response has started.
var metrics = serverTiming.GetMetrics();
foreach (var metric in metrics)
await Console.Out.WriteLineAsync(
$"- Name: {metric.Name}, Description: {metric.Description}, Duration: {metric.Duration}");
Create a singleton ActivitySource
that you can reuse throughout your application/library following these instructions.
internal static class Telemetry
{
internal static readonly ActivitySource Source = new("MySource");
}
Add the ActivitySource
names that should be monitored to the ActivitySources
on the ServerTimingOptions
.
// Add the Activity Source that should be monitored for AddServerTiming calls.
options.ActivitySources.Add(Telemetry.Source.Name);
When using an Activity
the AddServerTiming
extension method can be called to include the duration in the Server-Timing
header.
using (Telemetry.Source.StartActivity("Database").AddServerTiming("Queries and caching"))
{
// Perform database operations and cache results
}
The metric name in the Server-Timing
header will be the Name
of the Activity
. You can provide a description for the metric in the header by passing a value for the optional description
parameter on the AddServerTiming
extension method.
In the example above the resulting metric in the Server-Timing
header would be named Database and have a desc
of Queries and caching.
Database;dur=94.4693;desc="Queries and caching"
The services required for Activity monitoring are added when calling AddServerTiming
on the IServiceCollection
. This call is still required even when not using the IServerTiming
service directly.
services.AddServerTiming();
A callback service needs to implement the IServerTimingCallback
interface which has a single OnServerTiming
method. This method receives a ServerTimingEvent
instance with properties for the HttpContext
and a list of IMetric
instances. The list will contain all recorded IMetric
instances with their descriptions, including custom metrics, and regardless if descriptions and custom metrics were configured to be included in the Server-Timing
header. This allows you to capture these metrics in another service or package even if you did not want them send them in the Server-Timing
header.
public class SampleServerTimingCallback : IServerTimingCallback, IAsyncDisposable
{
public async Task OnServerTiming(ServerTimingEvent serverTimingEvent)
{
var displayUrl = serverTimingEvent.Context.DisplayUrl;
var metrics = serverTimingEvent.Metrics;
await Console.Out.WriteLineAsync($"Server-Timing for {displayUrl} has {metrics.Count} metrics");
foreach (var metric in metrics)
await Console.Out.WriteLineAsync(
$"Name: {metric.Name}, Description: {metric.Description}, Duration: {metric.Duration}");
}
public async ValueTask DisposeAsync() => await Console.Out.WriteLineAsync("Disposing callback");
}
Sample output from the above callback service.
Server-Timing for http://localhost:5000/api/sample?timing&desc&custom has 6 metrics:
- Name: external, Description: External metric timing, Duration: 25
- Name: miss, Description: Cache miss, Duration:
- Name: cache, Description: Cache writes, Duration: 39.321600000000004
- Name: db, Description: Database queries, Duration: 61.916700000000006
- Name: total, Description: Total, Duration: 166.9794
- Name: bus, Description: Send notification to bus, Duration: 22.0557
Disposing callback
The browser developer tools have a built-in visualization for metrics in the Server-Timing
header but often API requests are tested in a tool such as Postman. The sample folder contains a Postman collection with a visualizer that allows metrics to be visualized in a horizontal bar chart similar to that found in the browser developer tools.
Click the Visualize
button when viewing the response body in Postman to see the chart. The visualizer is applied at the collection level and will work with all requests in that collection. You can override the collection level visualizer for a specific request using the pm.visualizer.set
method.
The visualizer will display a message when the response does not contain a Server-Timing
header or no values are present in the header. Metrics that have a name but no duration are listed at the top of the visualization to provide context for the values in the chart.
Icon made by Freepik from www.flaticon.com