Skip to content

Commit

Permalink
Add Server-Timing middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Muhammad Rehan Saeed committed Dec 9, 2019
1 parent 9427b43 commit fc0051f
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 6 deletions.
24 changes: 22 additions & 2 deletions Source/Boxed.AspNetCore/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ public static class ApplicationBuilderExtensions
/// </summary>
/// <param name="application">The application builder.</param>
/// <returns>The same application builder.</returns>
public static IApplicationBuilder UseHttpException(this IApplicationBuilder application) => UseHttpException(application, null);
public static IApplicationBuilder UseHttpException(this IApplicationBuilder application) =>
UseHttpException(application, null);

/// <summary>
/// Allows the use of <see cref="HttpException"/> as an alternative method of returning an error result.
/// </summary>
/// <param name="application">The application builder.</param>
/// <param name="configureOptions">The middleware options.</param>
/// <returns>The same application builder.</returns>
public static IApplicationBuilder UseHttpException(this IApplicationBuilder application, Action<HttpExceptionMiddlewareOptions> configureOptions)
public static IApplicationBuilder UseHttpException(
this IApplicationBuilder application,
Action<HttpExceptionMiddlewareOptions> configureOptions)
{
if (application is null)
{
Expand All @@ -34,6 +37,23 @@ public static IApplicationBuilder UseHttpException(this IApplicationBuilder appl
return application.UseMiddleware<HttpExceptionMiddleware>(options);
}

/// <summary>
/// Measures the time the request takes to process and returns this in a Server-Timing trailing HTTP header.
/// It is used to surface any back-end server timing metrics (e.g. database read/write, CPU time, file system
/// access, etc.) to the developer tools in the user's browser.
/// </summary>
/// <param name="application">The application builder.</param>
/// <returns>The same application builder.</returns>
public static IApplicationBuilder UseServerTiming(this IApplicationBuilder application)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}

return application.UseMiddleware<ServerTimingMiddleware>();
}

/// <summary>
/// Executes the specified action if the specified <paramref name="condition"/> is <c>true</c> which can be
/// used to conditionally add to the request execution pipeline.
Expand Down
2 changes: 1 addition & 1 deletion Source/Boxed.AspNetCore/Boxed.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<PropertyGroup Label="Package">
<VersionPrefix>5.0.0</VersionPrefix>
<VersionPrefix>5.1.0</VersionPrefix>
<Authors>Muhammad Rehan Saeed (RehanSaeed.com)</Authors>
<Product>ASP.NET Core Framework Boxed</Product>
<Description>Provides ASP.NET Core middleware, MVC filters, extension methods and helper code for an ASP.NET Core project.</Description>
Expand Down
48 changes: 48 additions & 0 deletions Source/Boxed.AspNetCore/Middleware/ServerTimingMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Boxed.AspNetCore.Middleware
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

/// <summary>
/// Measures the time the request takes to process and returns this in a Server-Timing trailing HTTP header. It is
/// used to surface any back-end server timing metrics (e.g. database read/write, CPU time, file system access,
/// etc.) to the developer tools in the user's browser.
/// </summary>
/// <seealso cref="IMiddleware" />
public class ServerTimingMiddleware : IMiddleware
{
private const string ServerTimingHttpHeader = "Server-Timing";

/// <inheritdoc/>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (next == null)
{
throw new ArgumentNullException(nameof(next));
}

if (context.Response.SupportsTrailers())
{
context.Response.DeclareTrailer(ServerTimingHttpHeader);
var stopWatch = new Stopwatch();
stopWatch.Start();

await next(context).ConfigureAwait(false);

stopWatch.Stop();
context.Response.AppendTrailer(ServerTimingHttpHeader, $"app;dur={stopWatch.ElapsedMilliseconds}.0");
}
else
{
await next(context).ConfigureAwait(false);
}
}
}
}
10 changes: 7 additions & 3 deletions Tests/Boxed.AspNetCore.Test/ApplicationBuilderExtensionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ public ApplicationBuilderExtensionsTest()
}

[Fact]
public async Task UseIf_TrueCondition_ActionCalled()
public void UseServerTiming_NullApplication_ThrowsArgumentNullException() =>
Assert.Throws<ArgumentNullException>(() => Boxed.AspNetCore.ApplicationBuilderExtensions.UseServerTiming(null));

[Fact]
public async Task UseIf_TrueCondition_ActionCalledAsync()
{
var actionCalled = false;

Expand All @@ -40,7 +44,7 @@ public async Task UseIf_TrueCondition_ActionCalled()
}

[Fact]
public async Task UseIf_FalseCondition_ActionCalled()
public async Task UseIf_FalseCondition_ActionCalledAsync()
{
var actionCalled = false;

Expand All @@ -59,7 +63,7 @@ public async Task UseIf_FalseCondition_ActionCalled()
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task UseIfElse_TrueCondition_ActionCalled(bool condition)
public async Task UseIfElse_TrueCondition_ActionCalledAsync(bool condition)
{
var ifActionCalled = false;
var elseActionCalled = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace Boxed.AspNetCore.Test.Middleware
{
using System;
using System.Threading.Tasks;
using Boxed.AspNetCore.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Xunit;

public class ServerTimingMiddlewareTest
{
private readonly DefaultHttpContext context;
private readonly RequestDelegate next;

public ServerTimingMiddlewareTest()
{
this.context = new DefaultHttpContext();
this.next = x => Task.CompletedTask;
}

[Fact]
public void InvokeAsync_NullContext_ThrowsArgumentNullException() =>
Assert.ThrowsAsync<ArgumentNullException>(() => new ServerTimingMiddleware().InvokeAsync(null, this.next));

[Fact]
public void InvokeAsync_NullNext_ThrowsArgumentNullException() =>
Assert.ThrowsAsync<ArgumentNullException>(() => new ServerTimingMiddleware().InvokeAsync(this.context, null));

[Fact]
public async Task InvokeAsync_DoesntSupportTrailingHeaders_DontAddServerTimingHttpHeaderAsync()
{
var responseTrailersFeature = this.context.Features.Get<IHttpResponseTrailersFeature>();

await new ServerTimingMiddleware().InvokeAsync(this.context, this.next).ConfigureAwait(false);

Assert.Null(responseTrailersFeature);
}

[Fact]
public async Task InvokeAsync_SupportsTrailingHeaders_AddsServerTimingHttpHeaderAsync()
{
this.context.Features.Set<IHttpResponseTrailersFeature>(new ResponseTrailersFeature());
var responseTrailersFeature = this.context.Features.Get<IHttpResponseTrailersFeature>();

await new ServerTimingMiddleware().InvokeAsync(this.context, this.next).ConfigureAwait(false);

var header = Assert.Single(responseTrailersFeature.Trailers);
Assert.Equal("Server-Timing", header.Key);
Assert.Equal("app;dur=0.0", header.Value.ToString());
}

internal class ResponseTrailersFeature : IHttpResponseTrailersFeature
{
public IHeaderDictionary Trailers { get; set; } = new HeaderDictionary();
}
}
}

2 comments on commit fc0051f

@VictorioBerra
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need an AddServerTimingMiddleware extension to simplify registering ServerTimingMiddleware for UseServerTiming?

@RehanSaeed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I missed that.

Please sign in to comment.