Skip to content

Commit 81af9a7

Browse files
authored
Merge pull request #94 from serilog/request-completion-middleware
UseSerilogRequestLogging()
2 parents 4c826df + df5bf0d commit 81af9a7

File tree

14 files changed

+258
-9
lines changed

14 files changed

+258
-9
lines changed

samples/EarlyInitializationSample/Controllers/HomeController.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
using System;
22
using System.Diagnostics;
3+
using System.Threading;
34
using Microsoft.AspNetCore.Mvc;
45
using EarlyInitializationSample.Models;
56
using Microsoft.Extensions.Logging;
7+
using Serilog;
68

79
namespace EarlyInitializationSample.Controllers
810
{
911
public class HomeController : Controller
1012
{
13+
static int _callCount;
14+
1115
readonly ILogger<HomeController> _logger;
16+
readonly IDiagnosticContext _diagnosticContext;
1217

13-
public HomeController(ILogger<HomeController> logger)
18+
public HomeController(ILogger<HomeController> logger, IDiagnosticContext diagnosticContext)
1419
{
1520
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
21+
_diagnosticContext = diagnosticContext ?? throw new ArgumentNullException(nameof(diagnosticContext));
1622
}
1723

1824
public IActionResult Index()
1925
{
2026
_logger.LogInformation("Hello, world!");
27+
28+
_diagnosticContext.Add("IndexCallCount", Interlocked.Increment(ref _callCount));
29+
2130
return View();
2231
}
2332

2433
public IActionResult Privacy()
2534
{
26-
return View();
35+
throw new InvalidOperationException("Something went wrong.");
2736
}
2837

2938
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]

samples/EarlyInitializationSample/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ public static int Main(string[] args)
2121
Log.Logger = new LoggerConfiguration()
2222
.ReadFrom.Configuration(Configuration)
2323
.Enrich.FromLogContext()
24-
.WriteTo.Console()
24+
.WriteTo.Console(
25+
// {Properties:j} added:
26+
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} " +
27+
"{Properties:j}{NewLine}{Exception}")
2528
.CreateLogger();
2629

2730
try

samples/EarlyInitializationSample/Startup.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.AspNetCore.Mvc;
99
using Microsoft.Extensions.Configuration;
1010
using Microsoft.Extensions.DependencyInjection;
11+
using Serilog;
1112

1213
namespace EarlyInitializationSample
1314
{
@@ -46,6 +47,11 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
4647
app.UseExceptionHandler("/Home/Error");
4748
}
4849

50+
// Write streamlined request completion events, instead of the more verbose ones from the framework.
51+
// To use the default framework request logging instead, remove this line and set the "Microsoft"
52+
// level in appsettings.json to "Information".
53+
app.UseSerilogRequestLogging();
54+
4955
app.UseStaticFiles();
5056
app.UseCookiePolicy();
5157

samples/EarlyInitializationSample/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"MinimumLevel": {
44
"Default": "Information",
55
"Override": {
6-
"Microsoft": "Information",
6+
"Microsoft": "Warning",
77
"System": "Warning"
88
}
99
}

samples/InlineInitializationSample/Controllers/HomeController.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
using System;
22
using System.Diagnostics;
3+
using System.Threading;
34
using Microsoft.AspNetCore.Mvc;
45
using InlineInitializationSample.Models;
56
using Microsoft.Extensions.Logging;
7+
using Serilog;
68

79
namespace InlineInitializationSample.Controllers
810
{
911
public class HomeController : Controller
1012
{
13+
static int _callCount;
14+
1115
readonly ILogger<HomeController> _logger;
16+
readonly IDiagnosticContext _diagnosticContext;
1217

13-
public HomeController(ILogger<HomeController> logger)
18+
public HomeController(ILogger<HomeController> logger, IDiagnosticContext diagnosticContext)
1419
{
1520
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
21+
_diagnosticContext = diagnosticContext ?? throw new ArgumentNullException(nameof(diagnosticContext));
1622
}
1723

1824
public IActionResult Index()
1925
{
2026
_logger.LogInformation("Hello, world!");
27+
28+
_diagnosticContext.Add("IndexCallCount", Interlocked.Increment(ref _callCount));
29+
2130
return View();
2231
}
2332

2433
public IActionResult Privacy()
2534
{
26-
return View();
35+
throw new InvalidOperationException("Something went wrong.");
2736
}
2837

2938
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]

samples/InlineInitializationSample/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
1717
.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration
1818
.ReadFrom.Configuration(hostingContext.Configuration)
1919
.Enrich.FromLogContext()
20-
.WriteTo.Console());
20+
.WriteTo.Console(
21+
// {Properties:j} added:
22+
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} " +
23+
"{Properties:j}{NewLine}{Exception}"));
2124
}
2225
}

samples/InlineInitializationSample/Startup.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.AspNetCore.Mvc;
99
using Microsoft.Extensions.Configuration;
1010
using Microsoft.Extensions.DependencyInjection;
11+
using Serilog;
1112

1213
namespace InlineInitializationSample
1314
{
@@ -46,6 +47,11 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
4647
app.UseExceptionHandler("/Home/Error");
4748
}
4849

50+
// Write streamlined request completion events, instead of the more verbose ones from the framework.
51+
// To use the default framework request logging instead, remove this line and set the "Microsoft"
52+
// level in appsettings.json to "Information".
53+
app.UseSerilogRequestLogging();
54+
4955
app.UseStaticFiles();
5056
app.UseCookiePolicy();
5157

samples/InlineInitializationSample/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"MinimumLevel": {
44
"Default": "Information",
55
"Override": {
6-
"Microsoft": "Information",
6+
"Microsoft": "Warning",
77
"System": "Warning"
88
}
99
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2019 Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.Diagnostics;
18+
using System.Linq;
19+
using System.Threading.Tasks;
20+
using Microsoft.AspNetCore.Http;
21+
using Microsoft.AspNetCore.Http.Features;
22+
using Serilog.Events;
23+
using Serilog.Extensions.Hosting;
24+
using Serilog.Parsing;
25+
26+
namespace Serilog.AspNetCore
27+
{
28+
class RequestLoggingMiddleware
29+
{
30+
readonly RequestDelegate _next;
31+
readonly DiagnosticContext _diagnosticContext;
32+
readonly MessageTemplate _messageTemplate;
33+
readonly int _messageTemplatePlaceholderCount;
34+
35+
public RequestLoggingMiddleware(RequestDelegate next, DiagnosticContext diagnosticContext, RequestLoggingOptions options)
36+
{
37+
if (options == null) throw new ArgumentNullException(nameof(options));
38+
_next = next ?? throw new ArgumentNullException(nameof(next));
39+
_diagnosticContext = diagnosticContext ?? throw new ArgumentNullException(nameof(diagnosticContext));
40+
41+
_messageTemplate = new MessageTemplateParser().Parse(options.MessageTemplate);
42+
_messageTemplatePlaceholderCount = _messageTemplate.Tokens.OfType<PropertyToken>().Count();
43+
}
44+
45+
// ReSharper disable once UnusedMember.Global
46+
public async Task Invoke(HttpContext httpContext)
47+
{
48+
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
49+
50+
var start = Stopwatch.GetTimestamp();
51+
52+
var collector = _diagnosticContext.BeginCollection();
53+
try
54+
{
55+
await _next(httpContext);
56+
var elapsedMs = GetElapsedMilliseconds(start, Stopwatch.GetTimestamp());
57+
var statusCode = httpContext.Response.StatusCode;
58+
LogCompletion(httpContext, collector, statusCode, elapsedMs, null);
59+
}
60+
catch (Exception ex)
61+
// Never caught, because `LogCompletion()` returns false. This ensures e.g. the developer exception page is still
62+
// shown, although it does also mean we see a duplicate "unhandled exception" event from ASP.NET Core.
63+
when (LogCompletion(httpContext, collector, 500, GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()), ex))
64+
{
65+
}
66+
finally
67+
{
68+
collector.Dispose();
69+
}
70+
}
71+
72+
bool LogCompletion(HttpContext httpContext, DiagnosticContextCollector collector, int statusCode, double elapsedMs, Exception ex)
73+
{
74+
var level = statusCode > 499 ? LogEventLevel.Error : LogEventLevel.Information;
75+
76+
if (!Log.IsEnabled(level)) return false;
77+
78+
if (!collector.TryComplete(out var properties))
79+
properties = new List<LogEventProperty>();
80+
81+
properties.Capacity = properties.Count + _messageTemplatePlaceholderCount;
82+
83+
// Last-in (rightly) wins...
84+
properties.Add(new LogEventProperty("RequestMethod", new ScalarValue(httpContext.Request.Method)));
85+
properties.Add(new LogEventProperty("RequestPath", new ScalarValue(GetPath(httpContext))));
86+
properties.Add(new LogEventProperty("StatusCode", new ScalarValue(statusCode)));
87+
properties.Add(new LogEventProperty("Elapsed", new ScalarValue(elapsedMs)));
88+
var evt = new LogEvent(DateTimeOffset.Now, level, ex, _messageTemplate, properties);
89+
Log.Write(evt);
90+
91+
return false;
92+
}
93+
94+
static double GetElapsedMilliseconds(long start, long stop)
95+
{
96+
return (stop - start) * 1000 / (double)Stopwatch.Frequency;
97+
}
98+
99+
static string GetPath(HttpContext httpContext)
100+
{
101+
return httpContext.Features.Get<IHttpRequestFeature>()?.RawTarget ?? httpContext.Request.Path.ToString();
102+
}
103+
}
104+
}

src/Serilog.AspNetCore/AspNetCore/SerilogLoggerFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.ComponentModel;
1617
using Microsoft.Extensions.Logging;
1718
using Serilog.Debugging;
1819
using Serilog.Extensions.Logging;
@@ -23,6 +24,7 @@ namespace Serilog.AspNetCore
2324
/// Implements <see cref="ILoggerFactory"/> so that we can inject Serilog Logger.
2425
/// </summary>
2526
[Obsolete("Replaced with Serilog.Extensions.Logging.SerilogLoggerFactory")]
27+
[EditorBrowsable(EditorBrowsableState.Never)]
2628
public class SerilogLoggerFactory : ILoggerFactory
2729
{
2830
private readonly SerilogLoggerProvider _provider;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2019 Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
17+
namespace Serilog
18+
{
19+
class RequestLoggingOptions
20+
{
21+
public string MessageTemplate { get; }
22+
23+
public RequestLoggingOptions(string messageTemplate)
24+
{
25+
MessageTemplate = messageTemplate ?? throw new ArgumentNullException(nameof(messageTemplate));
26+
}
27+
}
28+
}

src/Serilog.AspNetCore/Serilog.AspNetCore.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
<ItemGroup>
2626
<PackageReference Include="Serilog" Version="2.8.0" />
27+
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0-*" />
2728
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.0-*" />
2829
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.0.0" />
2930
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0" />
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2019 Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using Microsoft.AspNetCore.Builder;
17+
using Serilog.AspNetCore;
18+
19+
namespace Serilog
20+
{
21+
/// <summary>
22+
/// Extends <see cref="IApplicationBuilder"/> with methods for configuring Serilog features.
23+
/// </summary>
24+
public static class SerilogApplicationBuilderExtensions
25+
{
26+
const string DefaultRequestCompletionMessageTemplate =
27+
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
28+
29+
/// <summary>
30+
/// Adds middleware for streamlined request logging. Instead of writing HTTP request information
31+
/// like method, path, timing, status code and exception details
32+
/// in several events, this middleware collects information during the request (including from
33+
/// <see cref="IDiagnosticContext"/>), and writes a single event at request completion. Add this
34+
/// in <c>Startup.cs</c> before any handlers whose activities should be logged.
35+
/// </summary>
36+
/// <param name="app">The application builder.</param>
37+
/// <param name="messageTemplate">The message template to use when logging request completion
38+
/// events. The default is
39+
/// <c>"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"</c>. The
40+
/// template can contain any of the placeholders from the default template, names of properties
41+
/// added by ASP.NET Core, and names of properties added to the <see cref="IDiagnosticContext"/>.
42+
/// </param>
43+
/// <returns>The application builder.</returns>
44+
public static IApplicationBuilder UseSerilogRequestLogging(
45+
this IApplicationBuilder app,
46+
string messageTemplate = DefaultRequestCompletionMessageTemplate)
47+
{
48+
if (app == null) throw new ArgumentNullException(nameof(app));
49+
if (messageTemplate == null) throw new ArgumentNullException(nameof(messageTemplate));
50+
return app.UseMiddleware<RequestLoggingMiddleware>(new RequestLoggingOptions(messageTemplate));
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)