Skip to content

Commit 51847c9

Browse files
authored
Merge pull request #488 from dotnet-maestro-bot/merge/release/2.2-to-master
[automated] Merge branch 'release/2.2' => 'master'
2 parents dd46e7e + 071a49e commit 51847c9

File tree

8 files changed

+200
-16
lines changed

8 files changed

+200
-16
lines changed

build/dependencies.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<MicrosoftNETCoreApp21PackageVersion>2.1.3</MicrosoftNETCoreApp21PackageVersion>
3737
<MicrosoftNETCoreApp22PackageVersion>2.2.0-preview2-26905-02</MicrosoftNETCoreApp22PackageVersion>
3838
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
39+
<MicrosoftNetHttpHeadersPackageVersion>3.0.0-alpha1-10495</MicrosoftNetHttpHeadersPackageVersion>
3940
<MoqPackageVersion>4.9.0</MoqPackageVersion>
4041
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
4142
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>

samples/HealthChecksSample/GCInfoHealthCheck.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ public GCInfoHealthCheck(IOptionsMonitor<GCInfoOptions> options)
6565
{ "Gen2Collections", GC.CollectionCount(2) },
6666
};
6767

68-
// Report failure if the allocated memory is >= the threshold
69-
var result = allocated >= options.Threshold;
68+
// Report failure if the allocated memory is >= the threshold. Negated because true == success
69+
var result = !(allocated >= options.Threshold);
7070

7171
return Task.FromResult(new HealthCheckResult(
7272
result,

samples/HealthChecksSample/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ static Program()
1515
{
1616
_scenarios = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
1717
{
18-
{ "", typeof(DBHealthStartup) },
18+
{ "", typeof(CustomWriterStartup) },
1919
{ "basic", typeof(BasicStartup) },
2020
{ "writer", typeof(CustomWriterStartup) },
2121
{ "liveness", typeof(LivenessProbeStartup) },

src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.AspNetCore.Http;
99
using Microsoft.Extensions.Diagnostics.HealthChecks;
1010
using Microsoft.Extensions.Options;
11+
using Microsoft.Net.Http.Headers;
1112

1213
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
1314
{
@@ -70,6 +71,15 @@ public async Task InvokeAsync(HttpContext httpContext)
7071

7172
httpContext.Response.StatusCode = statusCode;
7273

74+
if (!_healthCheckOptions.SuppressCacheHeaders)
75+
{
76+
// Similar to: https://github.com/aspnet/Security/blob/7b6c9cf0eeb149f2142dedd55a17430e7831ea99/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs#L377-L379
77+
var headers = httpContext.Response.Headers;
78+
headers[HeaderNames.CacheControl] = "no-store, no-cache";
79+
headers[HeaderNames.Pragma] = "no-cache";
80+
headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT";
81+
}
82+
7383
if (_healthCheckOptions.ResponseWriter != null)
7484
{
7585
await _healthCheckOptions.ResponseWriter(httpContext, result);
@@ -103,7 +113,7 @@ private static IHealthCheck[] FilterHealthChecks(
103113

104114
if (notFound.Count > 0)
105115
{
106-
var message =
116+
var message =
107117
$"The following health checks were not found: '{string.Join(", ", notFound)}'. " +
108118
$"Registered health checks: '{string.Join(", ", checks.Keys)}'.";
109119
throw new InvalidOperationException(message);

src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,13 @@ public class HealthCheckOptions
4646
/// of <see cref="HealthReport.Status"/> as a string.
4747
/// </remarks>
4848
public Func<HttpContext, HealthReport, Task> ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext;
49+
50+
/// <summary>
51+
/// Gets or sets a value that controls whether the health check middleware will add HTTP headers to prevent
52+
/// response caching. If the value is <c>false</c> the health check middleware will set or override the
53+
/// <c>Cache-Control</c>, <c>Expires</c>, and <c>Pragma</c> headers to prevent response caching. If the value
54+
/// is <c>true</c> the health check middleware will not modify the cache headers of the response.
55+
/// </summary>
56+
public bool SuppressCacheHeaders { get; set; }
4957
}
5058
}

src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<ItemGroup>
1919
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(MicrosoftAspNetCoreHttpAbstractionsPackageVersion)" />
2020
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
21+
<PackageReference Include="Microsoft.Net.Http.Headers" Version="$(MicrosoftNetHttpHeadersPackageVersion)" />
2122
</ItemGroup>
2223

2324
<ItemGroup>

src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections;
56
using System.Collections.Generic;
67
using System.Linq;
8+
using System.Text;
79
using System.Threading;
810
using System.Threading.Tasks;
911
using Microsoft.Extensions.DependencyInjection;
@@ -31,7 +33,7 @@ public DefaultHealthCheckService(
3133
// We're specifically going out of our way to do this at startup time. We want to make sure you
3234
// get any kind of health-check related error as early as possible. Waiting until someone
3335
// actually tries to **run** health checks would be real baaaaad.
34-
ValidateRegistrations(_options.Value.Registrations);
36+
ValidateRegistrations(_options.Value.Registrations);
3537
}
3638
public override async Task<HealthReport> CheckHealthAsync(
3739
Func<HealthCheckRegistration, bool> predicate,
@@ -44,6 +46,9 @@ public override async Task<HealthReport> CheckHealthAsync(
4446
var context = new HealthCheckContext();
4547
var entries = new Dictionary<string, HealthReportEntry>(StringComparer.OrdinalIgnoreCase);
4648

49+
var totalTime = ValueStopwatch.StartNew();
50+
Log.HealthCheckProcessingBegin(_logger);
51+
4752
foreach (var registration in registrations)
4853
{
4954
if (predicate != null && !predicate(registration))
@@ -63,7 +68,7 @@ public override async Task<HealthReport> CheckHealthAsync(
6368
context.Registration = registration;
6469

6570
Log.HealthCheckBegin(_logger, registration);
66-
71+
6772
HealthReportEntry entry;
6873
try
6974
{
@@ -76,6 +81,7 @@ public override async Task<HealthReport> CheckHealthAsync(
7681
result.Data);
7782

7883
Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime());
84+
Log.HealthCheckData(_logger, registration, entry);
7985
}
8086

8187
// Allow cancellation to propagate.
@@ -89,7 +95,9 @@ public override async Task<HealthReport> CheckHealthAsync(
8995
}
9096
}
9197

92-
return new HealthReport(entries);
98+
var report = new HealthReport(entries);
99+
Log.HealthCheckProcessingEnd(_logger, report.Status, totalTime.GetElapsedTime());
100+
return report;
93101
}
94102
}
95103

@@ -112,40 +120,144 @@ private static class Log
112120
{
113121
public static class EventIds
114122
{
115-
public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin");
116-
public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd");
117-
public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError");
123+
public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
124+
public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
125+
126+
public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
127+
public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
128+
public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
129+
public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
118130
}
119131

132+
private static readonly Action<ILogger, Exception> _healthCheckProcessingBegin = LoggerMessage.Define(
133+
LogLevel.Debug,
134+
EventIds.HealthCheckProcessingBegin,
135+
"Running health checks");
136+
137+
private static readonly Action<ILogger, double, HealthStatus, Exception> _healthCheckProcessingEnd = LoggerMessage.Define<double, HealthStatus>(
138+
LogLevel.Debug,
139+
EventIds.HealthCheckProcessingEnd,
140+
"Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}");
141+
120142
private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>(
121143
LogLevel.Debug,
122144
EventIds.HealthCheckBegin,
123145
"Running health check {HealthCheckName}");
124146

125-
private static readonly Action<ILogger, string, double, HealthStatus, Exception> _healthCheckEnd = LoggerMessage.Define<string, double, HealthStatus>(
147+
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEnd = LoggerMessage.Define<string, double, HealthStatus, string>(
126148
LogLevel.Debug,
127149
EventIds.HealthCheckEnd,
128-
"Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}");
150+
"Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'");
129151

130152
private static readonly Action<ILogger, string, double, Exception> _healthCheckError = LoggerMessage.Define<string, double>(
131153
LogLevel.Error,
132154
EventIds.HealthCheckError,
133155
"Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms");
134156

157+
public static void HealthCheckProcessingBegin(ILogger logger)
158+
{
159+
_healthCheckProcessingBegin(logger, null);
160+
}
161+
162+
public static void HealthCheckProcessingEnd(ILogger logger, HealthStatus status, TimeSpan duration)
163+
{
164+
_healthCheckProcessingEnd(logger, duration.TotalMilliseconds, status, null);
165+
}
166+
135167
public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration)
136168
{
137169
_healthCheckBegin(logger, registration.Name, null);
138170
}
139171

140172
public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration)
141173
{
142-
_healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, null);
174+
_healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
143175
}
144176

145177
public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration)
146178
{
147179
_healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception);
148180
}
181+
182+
public static void HealthCheckData(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry)
183+
{
184+
if (entry.Data.Count > 0 && logger.IsEnabled(LogLevel.Debug))
185+
{
186+
logger.Log(
187+
LogLevel.Debug,
188+
EventIds.HealthCheckData,
189+
new HealthCheckDataLogValue(registration.Name, entry.Data),
190+
null,
191+
(state, ex) => state.ToString());
192+
}
193+
}
194+
}
195+
196+
internal class HealthCheckDataLogValue : IReadOnlyList<KeyValuePair<string, object>>
197+
{
198+
private readonly string _name;
199+
private readonly List<KeyValuePair<string, object>> _values;
200+
201+
private string _formatted;
202+
203+
public HealthCheckDataLogValue(string name, IReadOnlyDictionary<string, object> values)
204+
{
205+
_name = name;
206+
_values = values.ToList();
207+
208+
// We add the name as a kvp so that you can filter by health check name in the logs.
209+
// This is the same parameter name used in the other logs.
210+
_values.Add(new KeyValuePair<string, object>("HealthCheckName", name));
211+
}
212+
213+
public KeyValuePair<string, object> this[int index]
214+
{
215+
get
216+
{
217+
if (index < 0 || index >= Count)
218+
{
219+
throw new IndexOutOfRangeException(nameof(index));
220+
}
221+
222+
return _values[index];
223+
}
224+
}
225+
226+
public int Count => _values.Count;
227+
228+
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
229+
{
230+
return _values.GetEnumerator();
231+
}
232+
233+
IEnumerator IEnumerable.GetEnumerator()
234+
{
235+
return _values.GetEnumerator();
236+
}
237+
238+
public override string ToString()
239+
{
240+
if (_formatted == null)
241+
{
242+
var builder = new StringBuilder();
243+
builder.AppendLine($"Health check data for {_name}:");
244+
245+
var values = _values;
246+
for (var i = 0; i < values.Count; i++)
247+
{
248+
var kvp = values[i];
249+
builder.Append(" ");
250+
builder.Append(kvp.Key);
251+
builder.Append(": ");
252+
253+
builder.AppendLine(kvp.Value?.ToString());
254+
}
255+
256+
_formatted = builder.ToString();
257+
}
258+
259+
return _formatted;
260+
}
149261
}
150262
}
151263
}

test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System.Net;
5-
using System.Threading.Tasks;
64
using Microsoft.AspNetCore.Builder;
75
using Microsoft.AspNetCore.Hosting;
86
using Microsoft.AspNetCore.Http;
97
using Microsoft.AspNetCore.TestHost;
108
using Microsoft.Extensions.DependencyInjection;
119
using Microsoft.Extensions.Diagnostics.HealthChecks;
10+
using Microsoft.Net.Http.Headers;
1211
using Newtonsoft.Json;
12+
using System.Net;
13+
using System.Threading.Tasks;
1314
using Xunit;
1415

1516
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
@@ -291,6 +292,57 @@ public async Task CanSetCustomStatusCodes()
291292
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
292293
}
293294

295+
[Fact]
296+
public async Task SetsCacheHeaders()
297+
{
298+
var builder = new WebHostBuilder()
299+
.Configure(app =>
300+
{
301+
app.UseHealthChecks("/health");
302+
})
303+
.ConfigureServices(services =>
304+
{
305+
services.AddHealthChecks();
306+
});
307+
var server = new TestServer(builder);
308+
var client = server.CreateClient();
309+
310+
var response = await client.GetAsync("/health");
311+
312+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
313+
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
314+
Assert.Equal("no-store, no-cache", response.Headers.CacheControl.ToString());
315+
Assert.Equal("no-cache", response.Headers.Pragma.ToString());
316+
Assert.Equal(new string[] { "Thu, 01 Jan 1970 00:00:00 GMT" }, response.Content.Headers.GetValues(HeaderNames.Expires));
317+
}
318+
319+
[Fact]
320+
public async Task CanSuppressCacheHeaders()
321+
{
322+
var builder = new WebHostBuilder()
323+
.Configure(app =>
324+
{
325+
app.UseHealthChecks("/health", new HealthCheckOptions()
326+
{
327+
SuppressCacheHeaders = true,
328+
});
329+
})
330+
.ConfigureServices(services =>
331+
{
332+
services.AddHealthChecks();
333+
});
334+
var server = new TestServer(builder);
335+
var client = server.CreateClient();
336+
337+
var response = await client.GetAsync("/health");
338+
339+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
340+
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
341+
Assert.Null(response.Headers.CacheControl);
342+
Assert.Empty(response.Headers.Pragma.ToString());
343+
Assert.False(response.Content.Headers.Contains(HeaderNames.Expires));
344+
}
345+
294346
[Fact]
295347
public async Task CanFilterChecks()
296348
{
@@ -363,7 +415,7 @@ public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort()
363415
{
364416
services.AddHealthChecks();
365417
});
366-
418+
367419
var server = new TestServer(builder);
368420
var client = server.CreateClient();
369421

0 commit comments

Comments
 (0)