Merge pull request #488 from dotnet-maestro-bot/merge/release/2.2-to-master

[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
Ryan Nowak 2018-09-24 11:21:46 -07:00 committed by GitHub
commit 51847c9f85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 16 deletions

View File

@ -36,6 +36,7 @@
<MicrosoftNETCoreApp21PackageVersion>2.1.3</MicrosoftNETCoreApp21PackageVersion> <MicrosoftNETCoreApp21PackageVersion>2.1.3</MicrosoftNETCoreApp21PackageVersion>
<MicrosoftNETCoreApp22PackageVersion>2.2.0-preview2-26905-02</MicrosoftNETCoreApp22PackageVersion> <MicrosoftNETCoreApp22PackageVersion>2.2.0-preview2-26905-02</MicrosoftNETCoreApp22PackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion> <MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
<MicrosoftNetHttpHeadersPackageVersion>3.0.0-alpha1-10495</MicrosoftNetHttpHeadersPackageVersion>
<MoqPackageVersion>4.9.0</MoqPackageVersion> <MoqPackageVersion>4.9.0</MoqPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion> <NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion> <NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>

View File

@ -65,8 +65,8 @@ namespace HealthChecksSample
{ "Gen2Collections", GC.CollectionCount(2) }, { "Gen2Collections", GC.CollectionCount(2) },
}; };
// Report failure if the allocated memory is >= the threshold // Report failure if the allocated memory is >= the threshold. Negated because true == success
var result = allocated >= options.Threshold; var result = !(allocated >= options.Threshold);
return Task.FromResult(new HealthCheckResult( return Task.FromResult(new HealthCheckResult(
result, result,

View File

@ -15,7 +15,7 @@ namespace HealthChecksSample
{ {
_scenarios = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase) _scenarios = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
{ {
{ "", typeof(DBHealthStartup) }, { "", typeof(CustomWriterStartup) },
{ "basic", typeof(BasicStartup) }, { "basic", typeof(BasicStartup) },
{ "writer", typeof(CustomWriterStartup) }, { "writer", typeof(CustomWriterStartup) },
{ "liveness", typeof(LivenessProbeStartup) }, { "liveness", typeof(LivenessProbeStartup) },

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{ {
@ -70,6 +71,15 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
httpContext.Response.StatusCode = statusCode; httpContext.Response.StatusCode = statusCode;
if (!_healthCheckOptions.SuppressCacheHeaders)
{
// Similar to: https://github.com/aspnet/Security/blob/7b6c9cf0eeb149f2142dedd55a17430e7831ea99/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs#L377-L379
var headers = httpContext.Response.Headers;
headers[HeaderNames.CacheControl] = "no-store, no-cache";
headers[HeaderNames.Pragma] = "no-cache";
headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT";
}
if (_healthCheckOptions.ResponseWriter != null) if (_healthCheckOptions.ResponseWriter != null)
{ {
await _healthCheckOptions.ResponseWriter(httpContext, result); await _healthCheckOptions.ResponseWriter(httpContext, result);
@ -103,7 +113,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
if (notFound.Count > 0) if (notFound.Count > 0)
{ {
var message = var message =
$"The following health checks were not found: '{string.Join(", ", notFound)}'. " + $"The following health checks were not found: '{string.Join(", ", notFound)}'. " +
$"Registered health checks: '{string.Join(", ", checks.Keys)}'."; $"Registered health checks: '{string.Join(", ", checks.Keys)}'.";
throw new InvalidOperationException(message); throw new InvalidOperationException(message);

View File

@ -46,5 +46,13 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
/// of <see cref="HealthReport.Status"/> as a string. /// of <see cref="HealthReport.Status"/> as a string.
/// </remarks> /// </remarks>
public Func<HttpContext, HealthReport, Task> ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; public Func<HttpContext, HealthReport, Task> ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext;
/// <summary>
/// Gets or sets a value that controls whether the health check middleware will add HTTP headers to prevent
/// response caching. If the value is <c>false</c> the health check middleware will set or override the
/// <c>Cache-Control</c>, <c>Expires</c>, and <c>Pragma</c> headers to prevent response caching. If the value
/// is <c>true</c> the health check middleware will not modify the cache headers of the response.
/// </summary>
public bool SuppressCacheHeaders { get; set; }
} }
} }

View File

@ -18,6 +18,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(MicrosoftAspNetCoreHttpAbstractionsPackageVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(MicrosoftAspNetCoreHttpAbstractionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="$(MicrosoftNetHttpHeadersPackageVersion)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -31,7 +33,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// We're specifically going out of our way to do this at startup time. We want to make sure you // We're specifically going out of our way to do this at startup time. We want to make sure you
// get any kind of health-check related error as early as possible. Waiting until someone // get any kind of health-check related error as early as possible. Waiting until someone
// actually tries to **run** health checks would be real baaaaad. // actually tries to **run** health checks would be real baaaaad.
ValidateRegistrations(_options.Value.Registrations); ValidateRegistrations(_options.Value.Registrations);
} }
public override async Task<HealthReport> CheckHealthAsync( public override async Task<HealthReport> CheckHealthAsync(
Func<HealthCheckRegistration, bool> predicate, Func<HealthCheckRegistration, bool> predicate,
@ -44,6 +46,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var context = new HealthCheckContext(); var context = new HealthCheckContext();
var entries = new Dictionary<string, HealthReportEntry>(StringComparer.OrdinalIgnoreCase); var entries = new Dictionary<string, HealthReportEntry>(StringComparer.OrdinalIgnoreCase);
var totalTime = ValueStopwatch.StartNew();
Log.HealthCheckProcessingBegin(_logger);
foreach (var registration in registrations) foreach (var registration in registrations)
{ {
if (predicate != null && !predicate(registration)) if (predicate != null && !predicate(registration))
@ -63,7 +68,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
context.Registration = registration; context.Registration = registration;
Log.HealthCheckBegin(_logger, registration); Log.HealthCheckBegin(_logger, registration);
HealthReportEntry entry; HealthReportEntry entry;
try try
{ {
@ -76,6 +81,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
result.Data); result.Data);
Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime()); Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime());
Log.HealthCheckData(_logger, registration, entry);
} }
// Allow cancellation to propagate. // Allow cancellation to propagate.
@ -89,7 +95,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
} }
} }
return new HealthReport(entries); var report = new HealthReport(entries);
Log.HealthCheckProcessingEnd(_logger, report.Status, totalTime.GetElapsedTime());
return report;
} }
} }
@ -112,26 +120,50 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ {
public static class EventIds public static class EventIds
{ {
public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError");
public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
} }
private static readonly Action<ILogger, Exception> _healthCheckProcessingBegin = LoggerMessage.Define(
LogLevel.Debug,
EventIds.HealthCheckProcessingBegin,
"Running health checks");
private static readonly Action<ILogger, double, HealthStatus, Exception> _healthCheckProcessingEnd = LoggerMessage.Define<double, HealthStatus>(
LogLevel.Debug,
EventIds.HealthCheckProcessingEnd,
"Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}");
private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>( private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.HealthCheckBegin, EventIds.HealthCheckBegin,
"Running health check {HealthCheckName}"); "Running health check {HealthCheckName}");
private static readonly Action<ILogger, string, double, HealthStatus, Exception> _healthCheckEnd = LoggerMessage.Define<string, double, HealthStatus>( private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEnd = LoggerMessage.Define<string, double, HealthStatus, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.HealthCheckEnd, EventIds.HealthCheckEnd,
"Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}"); "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'");
private static readonly Action<ILogger, string, double, Exception> _healthCheckError = LoggerMessage.Define<string, double>( private static readonly Action<ILogger, string, double, Exception> _healthCheckError = LoggerMessage.Define<string, double>(
LogLevel.Error, LogLevel.Error,
EventIds.HealthCheckError, EventIds.HealthCheckError,
"Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms"); "Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms");
public static void HealthCheckProcessingBegin(ILogger logger)
{
_healthCheckProcessingBegin(logger, null);
}
public static void HealthCheckProcessingEnd(ILogger logger, HealthStatus status, TimeSpan duration)
{
_healthCheckProcessingEnd(logger, duration.TotalMilliseconds, status, null);
}
public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration) public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration)
{ {
_healthCheckBegin(logger, registration.Name, null); _healthCheckBegin(logger, registration.Name, null);
@ -139,13 +171,93 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration) public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration)
{ {
_healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, null); _healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
} }
public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration) public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration)
{ {
_healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception); _healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception);
} }
public static void HealthCheckData(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry)
{
if (entry.Data.Count > 0 && logger.IsEnabled(LogLevel.Debug))
{
logger.Log(
LogLevel.Debug,
EventIds.HealthCheckData,
new HealthCheckDataLogValue(registration.Name, entry.Data),
null,
(state, ex) => state.ToString());
}
}
}
internal class HealthCheckDataLogValue : IReadOnlyList<KeyValuePair<string, object>>
{
private readonly string _name;
private readonly List<KeyValuePair<string, object>> _values;
private string _formatted;
public HealthCheckDataLogValue(string name, IReadOnlyDictionary<string, object> values)
{
_name = name;
_values = values.ToList();
// We add the name as a kvp so that you can filter by health check name in the logs.
// This is the same parameter name used in the other logs.
_values.Add(new KeyValuePair<string, object>("HealthCheckName", name));
}
public KeyValuePair<string, object> this[int index]
{
get
{
if (index < 0 || index >= Count)
{
throw new IndexOutOfRangeException(nameof(index));
}
return _values[index];
}
}
public int Count => _values.Count;
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return _values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _values.GetEnumerator();
}
public override string ToString()
{
if (_formatted == null)
{
var builder = new StringBuilder();
builder.AppendLine($"Health check data for {_name}:");
var values = _values;
for (var i = 0; i < values.Count; i++)
{
var kvp = values[i];
builder.Append(" ");
builder.Append(kvp.Key);
builder.Append(": ");
builder.AppendLine(kvp.Value?.ToString());
}
_formatted = builder.ToString();
}
return _formatted;
}
} }
} }
} }

View File

@ -1,15 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Net;
using System.Threading.Tasks;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
@ -291,6 +292,57 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
} }
[Fact]
public async Task SetsCacheHeaders()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
Assert.Equal("no-store, no-cache", response.Headers.CacheControl.ToString());
Assert.Equal("no-cache", response.Headers.Pragma.ToString());
Assert.Equal(new string[] { "Thu, 01 Jan 1970 00:00:00 GMT" }, response.Content.Headers.GetValues(HeaderNames.Expires));
}
[Fact]
public async Task CanSuppressCacheHeaders()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
SuppressCacheHeaders = true,
});
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
Assert.Null(response.Headers.CacheControl);
Assert.Empty(response.Headers.Pragma.ToString());
Assert.False(response.Content.Headers.Contains(HeaderNames.Expires));
}
[Fact] [Fact]
public async Task CanFilterChecks() public async Task CanFilterChecks()
{ {
@ -363,7 +415,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{ {
services.AddHealthChecks(); services.AddHealthChecks();
}); });
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient(); var client = server.CreateClient();