From afae173c753106a447e77f0c682b4384c19f7d8d Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Mon, 16 Apr 2018 16:56:10 -0700 Subject: [PATCH 01/57] Update version number to 2.2.0 --- version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.props b/version.props index 9124b26738..db5c0a52bf 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ - 2.1.0 - preview3 + 2.2.0 + preview1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 From 7b1517f5d03b486dcaf01e52a70985982aa809ae Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Mon, 23 Apr 2018 12:04:33 -0700 Subject: [PATCH 02/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index f15c01d8e9..64fd896c7c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,40 +3,40 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-rc1-15774 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 + 2.2.0-preview1-17037 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 + 2.2.0-preview1-34029 2.0.0 - 2.1.0-rc1-26419-02 + 2.1.0-preview3-26413-05 15.6.1 4.7.49 2.0.1 11.0.2 - 4.5.0-rc1-26419-03 - 1.6.0-rc1-26419-03 + 4.5.0-preview3-26413-02 + 1.6.0-preview3-26413-02 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index b419d767b9..f27a67b442 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview3-17018 -commithash:af264ca131f212b5ba8aafbc5110fc0fc510a2be +version:2.2.0-preview1-17037 +commithash:557055a86cbdc359c97d4fb1c2d23a3dc7ae731e From 9cc72f7af159e84c695935d13ff1b8f3ba669e7e Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 29 Apr 2018 12:12:16 -0700 Subject: [PATCH 03/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 60 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 64fd896c7c..7bc727326a 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,40 +3,40 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17037 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 - 2.2.0-preview1-34029 + 2.2.0-preview1-17042 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 + 2.2.0-preview1-34066 2.0.0 - 2.1.0-preview3-26413-05 + 2.2.0-preview1-26424-04 15.6.1 4.7.49 - 2.0.1 + 2.0.3 11.0.2 - 4.5.0-preview3-26413-02 - 1.6.0-preview3-26413-02 + 4.5.0-preview3-26423-04 + 1.6.0-preview3-26423-04 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index f27a67b442..335e579e06 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17037 -commithash:557055a86cbdc359c97d4fb1c2d23a3dc7ae731e +version:2.2.0-preview1-17042 +commithash:edf0705d014293c260de763543784330514db9a3 From c997fa8365b7b78d6a4a358835374799addbddfb Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 6 May 2018 12:11:26 -0700 Subject: [PATCH 04/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 52 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 ++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 7bc727326a..1f8dd233be 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,32 +3,32 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17042 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 - 2.2.0-preview1-34066 + 2.2.0-preview1-17047 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 + 2.2.0-preview1-34135 2.0.0 2.2.0-preview1-26424-04 15.6.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 335e579e06..a16d4b9ee4 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17042 -commithash:edf0705d014293c260de763543784330514db9a3 +version:2.2.0-preview1-17047 +commithash:e1957b52ddc8b62bd39c5c400322fccb5364624c From 519ca4adb0cd2041b4f7a769624bafd6d5da81ed Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Mon, 7 May 2018 15:10:05 -0700 Subject: [PATCH 05/57] Upgrade to netcoreapp22 --- Directory.Build.targets | 5 +- build/dependencies.props | 55 ++++++++++--------- build/repo.props | 3 +- korebuild-lock.txt | 4 +- .../DatabaseErrorPageSample.csproj | 4 +- .../DeveloperExceptionPageSample.csproj | 4 +- samples/ElmPageSample/ElmPageSample.csproj | 4 +- .../ExceptionHandlerSample.csproj | 4 +- .../MiddlewareAnalysisSample.csproj | 4 +- .../StatusCodePagesSample.csproj | 4 +- .../WelcomePageSample.csproj | 4 +- test/Directory.Build.props | 4 +- 12 files changed, 52 insertions(+), 47 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 53b3f6e1da..78626b773e 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,7 +1,10 @@ - + $(MicrosoftNETCoreApp20PackageVersion) $(MicrosoftNETCoreApp21PackageVersion) + $(MicrosoftNETCoreApp22PackageVersion) $(NETStandardLibrary20PackageVersion) + + 99.9 diff --git a/build/dependencies.props b/build/dependencies.props index 1f8dd233be..367e943ff7 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,36 +1,37 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17047 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 - 2.2.0-preview1-34135 + 2.2.0-preview1-17048 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 + 2.2.0-preview1-34140 2.0.0 2.2.0-preview1-26424-04 + 2.2.0-preview1-26502-01 15.6.1 4.7.49 2.0.3 diff --git a/build/repo.props b/build/repo.props index aae3d7376e..4a805f4722 100644 --- a/build/repo.props +++ b/build/repo.props @@ -1,4 +1,4 @@ - + @@ -13,5 +13,6 @@ + diff --git a/korebuild-lock.txt b/korebuild-lock.txt index a16d4b9ee4..2573a03995 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17047 -commithash:e1957b52ddc8b62bd39c5c400322fccb5364624c +version:2.2.0-preview1-17048 +commithash:de14a0ee5fb48508ee8a29c14280a2928f8dabf8 diff --git a/samples/DatabaseErrorPageSample/DatabaseErrorPageSample.csproj b/samples/DatabaseErrorPageSample/DatabaseErrorPageSample.csproj index 9ddf86922c..b49ed01565 100644 --- a/samples/DatabaseErrorPageSample/DatabaseErrorPageSample.csproj +++ b/samples/DatabaseErrorPageSample/DatabaseErrorPageSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.0;netcoreapp2.1 + net461;netcoreapp2.2 diff --git a/samples/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj b/samples/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj index 3c9fff2be0..f03b4f3955 100644 --- a/samples/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj +++ b/samples/DeveloperExceptionPageSample/DeveloperExceptionPageSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.0;netcoreapp2.1 + net461;netcoreapp2.2 diff --git a/samples/ElmPageSample/ElmPageSample.csproj b/samples/ElmPageSample/ElmPageSample.csproj index 7946494dae..bd99280b70 100644 --- a/samples/ElmPageSample/ElmPageSample.csproj +++ b/samples/ElmPageSample/ElmPageSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.0;netcoreapp2.1 + net461;netcoreapp2.2 diff --git a/samples/ExceptionHandlerSample/ExceptionHandlerSample.csproj b/samples/ExceptionHandlerSample/ExceptionHandlerSample.csproj index 2b7f85b349..fec6cb0ebc 100644 --- a/samples/ExceptionHandlerSample/ExceptionHandlerSample.csproj +++ b/samples/ExceptionHandlerSample/ExceptionHandlerSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.0;netcoreapp2.1 + net461;netcoreapp2.2 diff --git a/samples/MiddlewareAnalysisSample/MiddlewareAnalysisSample.csproj b/samples/MiddlewareAnalysisSample/MiddlewareAnalysisSample.csproj index b7472cbccc..b7065db9d9 100644 --- a/samples/MiddlewareAnalysisSample/MiddlewareAnalysisSample.csproj +++ b/samples/MiddlewareAnalysisSample/MiddlewareAnalysisSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.0;netcoreapp2.1 + net461;netcoreapp2.2 diff --git a/samples/StatusCodePagesSample/StatusCodePagesSample.csproj b/samples/StatusCodePagesSample/StatusCodePagesSample.csproj index 3c9fff2be0..f03b4f3955 100644 --- a/samples/StatusCodePagesSample/StatusCodePagesSample.csproj +++ b/samples/StatusCodePagesSample/StatusCodePagesSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.0;netcoreapp2.1 + net461;netcoreapp2.2 diff --git a/samples/WelcomePageSample/WelcomePageSample.csproj b/samples/WelcomePageSample/WelcomePageSample.csproj index 3c9fff2be0..f03b4f3955 100644 --- a/samples/WelcomePageSample/WelcomePageSample.csproj +++ b/samples/WelcomePageSample/WelcomePageSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.0;netcoreapp2.1 + net461;netcoreapp2.2 diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 6ab2251a4a..3a373e2cb8 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,9 +2,9 @@ - netcoreapp2.1 + netcoreapp2.2 $(DeveloperBuildTestTfms) - netcoreapp2.1;netcoreapp2.0 + $(StandardTestTfms);net461 From 8dce01f654962580e22e7c96ad4f313574407a40 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 13 May 2018 14:04:40 -0700 Subject: [PATCH 06/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 62 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 367e943ff7..e37e6ac2e9 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,43 +1,43 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17048 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 - 2.2.0-preview1-34140 + 2.2.0-preview1-17051 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 + 2.2.0-preview1-34184 2.0.0 - 2.2.0-preview1-26424-04 - 2.2.0-preview1-26502-01 + 2.1.0-rc1 + 2.2.0-preview1-26509-06 15.6.1 4.7.49 2.0.3 11.0.2 - 4.5.0-preview3-26423-04 - 1.6.0-preview3-26423-04 + 4.6.0-preview1-26508-04 + 1.7.0-preview1-26508-04 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 2573a03995..89629b454c 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17048 -commithash:de14a0ee5fb48508ee8a29c14280a2928f8dabf8 +version:2.2.0-preview1-17051 +commithash:253c3a480063bc3abaa5cde42f6e27b58457ef9b From 5f7102a4692d5400914e1ee27699530d28261a1c Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 20 May 2018 19:27:54 +0000 Subject: [PATCH 07/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 52 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 ++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index e37e6ac2e9..047c0e8907 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,32 +3,32 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17051 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 - 2.2.0-preview1-34184 + 2.2.0-preview1-17060 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 + 2.2.0-preview1-34255 2.0.0 2.1.0-rc1 2.2.0-preview1-26509-06 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 89629b454c..cf2fff7def 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17051 -commithash:253c3a480063bc3abaa5cde42f6e27b58457ef9b +version:2.2.0-preview1-17060 +commithash:25b4b134d6f8f7b461928f0d495cfc695ccabb5b From d763d969405543049ae83420a9dfa2a67a841ca0 Mon Sep 17 00:00:00 2001 From: "Nate McMaster (automated)" Date: Fri, 25 May 2018 16:13:45 -0700 Subject: [PATCH 08/57] Update bootstrapper scripts (automated commit) [ci skip] --- run.ps1 | 25 +++++++++++++++++++------ run.sh | 33 +++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/run.ps1 b/run.ps1 index 27dcf848f8..3b27382468 100644 --- a/run.ps1 +++ b/run.ps1 @@ -26,12 +26,18 @@ The base url where build tools can be downloaded. Overrides the value from the c .PARAMETER Update Updates KoreBuild to the latest version even if a lock file is present. +.PARAMETER Reinstall +Re-installs KoreBuild + .PARAMETER ConfigFile The path to the configuration file that stores values. Defaults to korebuild.json. .PARAMETER ToolsSourceSuffix The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. +.PARAMETER CI +Sets up CI specific settings and variables. + .PARAMETER Arguments Arguments to be passed to the command @@ -65,8 +71,10 @@ param( [string]$ToolsSource, [Alias('u')] [switch]$Update, - [string]$ConfigFile, + [switch]$Reinstall, [string]$ToolsSourceSuffix, + [string]$ConfigFile = $null, + [switch]$CI, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) @@ -93,6 +101,10 @@ function Get-KoreBuild { $version = $version.TrimStart('version:').Trim() $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) + if ($Reinstall -and (Test-Path $korebuildPath)) { + Remove-Item -Force -Recurse $korebuildPath + } + if (!(Test-Path $korebuildPath)) { Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" New-Item -ItemType Directory -Path $korebuildPath | Out-Null @@ -101,9 +113,9 @@ function Get-KoreBuild { try { $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix - if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) { + if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) { # Use built-in commands where possible as they are cross-plat compatible - Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath + Microsoft.PowerShell.Archive\Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath } else { # Fallback to old approach for old installations of PowerShell @@ -167,8 +179,9 @@ if (Test-Path $ConfigFile) { } } catch { - Write-Warning "$ConfigFile could not be read. Its settings will be ignored." - Write-Warning $Error[0] + Write-Host -ForegroundColor Red $Error[0] + Write-Error "$ConfigFile contains invalid JSON." + exit 1 } } @@ -188,7 +201,7 @@ $korebuildPath = Get-KoreBuild Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') try { - Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile + Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile -CI:$CI Invoke-KoreBuildCommand $Command @Arguments } finally { diff --git a/run.sh b/run.sh index 834961fc3a..02aac15874 100755 --- a/run.sh +++ b/run.sh @@ -14,10 +14,12 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" [ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" verbose=false update=false +reinstall=false repo_path="$DIR" channel='' tools_source='' tools_source_suffix='' +ci=false # # Functions @@ -38,6 +40,8 @@ __usage() { echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." echo " -u|--update Update to the latest KoreBuild even if the lock file is present." + echo " --reinstall Reinstall KoreBuild." + echo " --ci Apply CI specific settings and environment variables." echo "" echo "Description:" echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." @@ -62,6 +66,10 @@ get_korebuild() { version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" + if [ "$reinstall" = true ] && [ -d "$korebuild_path" ]; then + rm -rf "$korebuild_path" + fi + { if [ ! -d "$korebuild_path" ]; then mkdir -p "$korebuild_path" @@ -175,6 +183,12 @@ while [[ $# -gt 0 ]]; do -u|--update|-Update) update=true ;; + --reinstall|-[Rr]einstall) + reinstall=true + ;; + --ci|-[Cc][Ii]) + ci=true + ;; --verbose|-Verbose) verbose=true ;; @@ -206,17 +220,28 @@ if [ -f "$config_file" ]; then config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" else - __warn "$config_file is invalid JSON. Its settings will be ignored." + _error "$config_file contains invalid JSON." + exit 1 fi elif __machine_has python ; then if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" else - __warn "$config_file is invalid JSON. Its settings will be ignored." + _error "$config_file contains invalid JSON." + exit 1 + fi + elif __machine_has python3 ; then + if python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then + config_channel="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" + config_tools_source="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" + else + _error "$config_file contains invalid JSON." + exit 1 fi else - __warn 'Missing required command: jq or pyton. Could not parse the JSON file. Its settings will be ignored.' + _error 'Missing required command: jq or python. Could not parse the JSON file.' + exit 1 fi [ ! -z "${config_channel:-}" ] && channel="$config_channel" @@ -227,5 +252,5 @@ fi [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' get_korebuild -set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" +set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" "$ci" invoke_korebuild_command "$command" "$@" From 2f297de23215f981d6ed521868e4f235ee8afa48 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 27 May 2018 19:10:46 +0000 Subject: [PATCH 09/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 047c0e8907..bc30895866 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,41 +3,41 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17060 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 - 2.2.0-preview1-34255 + 2.2.0-preview1-17064 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 + 2.2.0-preview1-34326 2.0.0 2.1.0-rc1 - 2.2.0-preview1-26509-06 + 2.2.0-preview1-26526-03 15.6.1 4.7.49 2.0.3 11.0.2 - 4.6.0-preview1-26508-04 - 1.7.0-preview1-26508-04 + 4.6.0-preview1-26525-01 + 1.7.0-preview1-26525-01 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index cf2fff7def..3028b66761 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17060 -commithash:25b4b134d6f8f7b461928f0d495cfc695ccabb5b +version:2.2.0-preview1-17064 +commithash:5380a2461b135b261646f31d1c919ab0a7b577a8 From d8ecc6c9679561a6a485ee6728969de126d6a11e Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 3 Jun 2018 19:10:34 +0000 Subject: [PATCH 10/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 60 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index bc30895866..a042bea5f6 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,41 +3,41 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17064 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 - 2.2.0-preview1-34326 + 2.2.0-preview1-17067 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 + 2.2.0-preview1-34373 2.0.0 - 2.1.0-rc1 - 2.2.0-preview1-26526-03 + 2.1.0 + 2.2.0-preview1-26531-03 15.6.1 4.7.49 2.0.3 11.0.2 - 4.6.0-preview1-26525-01 - 1.7.0-preview1-26525-01 + 4.6.0-preview1-26531-03 + 1.7.0-preview1-26531-03 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 3028b66761..06ba6285b7 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17064 -commithash:5380a2461b135b261646f31d1c919ab0a7b577a8 +version:2.2.0-preview1-17067 +commithash:2af0e2e3d02329b4f0290061ab9bd8c7ca1aa26f From 3296a4f040d7defe379eade836d0ac4bbcf1bfa7 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 5 Jun 2018 22:31:17 -0700 Subject: [PATCH 11/57] Add certificate names for code signing --- Directory.Build.props | 2 ++ korebuild-lock.txt | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6409867c7e..6fc59be2d0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,6 +14,8 @@ $(MSBuildThisFileDirectory) $(MSBuildThisFileDirectory)build\Key.snk true + Microsoft + MicrosoftNuGet true true diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 06ba6285b7..b679b80427 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17067 -commithash:2af0e2e3d02329b4f0290061ab9bd8c7ca1aa26f +version:2.2.0-preview1-17075 +commithash:d9f07c7f313a0af1d49f003f5424b4dbbdd3e09f From db3e237144fc4987df134e7f2974dc5b243d2287 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Thu, 7 Jun 2018 19:31:22 +0000 Subject: [PATCH 12/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index a042bea5f6..4c7a3f2e3c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,41 +3,41 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17067 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 - 2.2.0-preview1-34373 + 2.2.0-preview1-17081 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 + 2.2.0-preview1-34411 2.0.0 2.1.0 - 2.2.0-preview1-26531-03 + 2.2.0-preview1-26606-01 15.6.1 4.7.49 2.0.3 11.0.2 - 4.6.0-preview1-26531-03 - 1.7.0-preview1-26531-03 + 4.6.0-preview1-26605-01 + 1.7.0-preview1-26605-01 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index b679b80427..deb7e546f0 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17075 -commithash:d9f07c7f313a0af1d49f003f5424b4dbbdd3e09f +version:2.2.0-preview1-17081 +commithash:73f09c256e2a54270951562ecc0ef4a953926c36 From 191d2651fc3a9085df23327d7012e49e0e73adbd Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Wed, 13 Jun 2018 10:54:20 -0700 Subject: [PATCH 13/57] Set 2.1 baselines --- .../baseline.netcore.json | 2 +- .../baseline.netcore.json | 24 +- .../baseline.netcore.json | 2 +- .../baseline.netcore.json | 115 ++++++ .../baseline.netcore.json | 2 +- .../baseline.netcore.json | 2 +- .../baseline.netcore.json | 377 ++++++++++++++++++ .../baseline.netcore.json | 334 ++++++++++++++++ 8 files changed, 842 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json diff --git a/src/Microsoft.AspNetCore.Diagnostics.Abstractions/baseline.netcore.json b/src/Microsoft.AspNetCore.Diagnostics.Abstractions/baseline.netcore.json index 4259578c6b..6cbf6b065c 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.Abstractions/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Diagnostics.Abstractions/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.Abstractions, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.Diagnostics.CompilationFailure", diff --git a/src/Microsoft.AspNetCore.Diagnostics.Elm/baseline.netcore.json b/src/Microsoft.AspNetCore.Diagnostics.Elm/baseline.netcore.json index 1a855c3487..40dd1cfb96 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.Elm/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Diagnostics.Elm/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.Elm, Version=0.3.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.Elm, Version=0.4.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.AttributeValue", @@ -864,6 +864,17 @@ "Microsoft.Extensions.Logging.ILoggerProvider" ], "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, { "Kind": "Method", "Name": "CreateLogger", @@ -880,17 +891,6 @@ "Visibility": "Public", "GenericParameter": [] }, - { - "Kind": "Method", - "Name": "Dispose", - "Parameters": [], - "ReturnType": "System.Void", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "System.IDisposable", - "Visibility": "Public", - "GenericParameter": [] - }, { "Kind": "Constructor", "Name": ".ctor", diff --git a/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore/baseline.netcore.json b/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore/baseline.netcore.json index c697aebee0..287e144156 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.AttributeValue", diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json new file mode 100644 index 0000000000..c0e8deddd1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json @@ -0,0 +1,115 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "InvokeAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "healthCheckOptions", + "Type": "Microsoft.Extensions.Options.IOptions" + }, + { + "Name": "healthCheckService", + "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.HealthCheckAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseHealthChecks", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "path", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Diagnostics/baseline.netcore.json b/src/Microsoft.AspNetCore.Diagnostics/baseline.netcore.json index 277e25a13a..eeea95de0b 100644 --- a/src/Microsoft.AspNetCore.Diagnostics/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Diagnostics/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.AttributeValue", diff --git a/src/Microsoft.AspNetCore.MiddlewareAnalysis/baseline.netcore.json b/src/Microsoft.AspNetCore.MiddlewareAnalysis/baseline.netcore.json index 1bf0949d53..fe220573ae 100644 --- a/src/Microsoft.AspNetCore.MiddlewareAnalysis/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.MiddlewareAnalysis/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.MiddlewareAnalysis, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.MiddlewareAnalysis, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.AnalysisServiceCollectionExtensions", diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json new file mode 100644 index 0000000000..7792748d55 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json @@ -0,0 +1,377 @@ +{ + "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Status", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Exception", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Description", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Data", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unhealthy", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unhealthy", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unhealthy", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + }, + { + "Name": "data", + "Type": "System.Collections.Generic.IReadOnlyDictionary" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unhealthy", + "Parameters": [ + { + "Name": "exception", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unhealthy", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + }, + { + "Name": "exception", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unhealthy", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + }, + { + "Name": "exception", + "Type": "System.Exception" + }, + { + "Name": "data", + "Type": "System.Collections.Generic.IReadOnlyDictionary" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Healthy", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Healthy", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Healthy", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + }, + { + "Name": "data", + "Type": "System.Collections.Generic.IReadOnlyDictionary" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Degraded", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Degraded", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Degraded", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + }, + { + "Name": "data", + "Type": "System.Collections.Generic.IReadOnlyDictionary" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Degraded", + "Parameters": [ + { + "Name": "exception", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Degraded", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + }, + { + "Name": "exception", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Degraded", + "Parameters": [ + { + "Name": "description", + "Type": "System.String" + }, + { + "Name": "exception", + "Type": "System.Exception" + }, + { + "Name": "data", + "Type": "System.Collections.Generic.IReadOnlyDictionary" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "status", + "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus" + }, + { + "Name": "exception", + "Type": "System.Exception" + }, + { + "Name": "description", + "Type": "System.String" + }, + { + "Name": "data", + "Type": "System.Collections.Generic.IReadOnlyDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Unknown", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Failed", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "Unhealthy", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + }, + { + "Kind": "Field", + "Name": "Degraded", + "Parameters": [], + "GenericParameter": [], + "Literal": "3" + }, + { + "Kind": "Field", + "Name": "Healthy", + "Parameters": [], + "GenericParameter": [], + "Literal": "4" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CheckHealthAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json b/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json new file mode 100644 index 0000000000..4938a12605 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json @@ -0,0 +1,334 @@ +{ + "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.HealthChecksBuilderAddCheckExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddCheck", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "check", + "Type": "System.Func>" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCheck", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "check", + "Type": "System.Func>" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.HealthCheckServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddHealthChecks", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.CompositeHealthCheckResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Results", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Status", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "results", + "Type": "System.Collections.Generic.IReadOnlyDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheck", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CheckHealthAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "check", + "Type": "System.Func>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Checks", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CheckHealthAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CheckHealthAsync", + "Parameters": [ + { + "Name": "checks", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "healthChecks", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "healthChecks", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILogger" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Services", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Checks", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CheckHealthAsync", + "Parameters": [ + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CheckHealthAsync", + "Parameters": [ + { + "Name": "checks", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "cancellationToken", + "Type": "System.Threading.CancellationToken", + "DefaultValue": "default(System.Threading.CancellationToken)" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file From c5eda241ea9186ed66e145d00e4c44c7ec8af334 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Thu, 7 Jun 2018 15:44:09 -0700 Subject: [PATCH 14/57] Adding VSTS file --- .vsts-pipelines/builds/ci-internal.yml | 13 ++++++++++++ .vsts-pipelines/builds/ci-public.yml | 28 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .vsts-pipelines/builds/ci-internal.yml create mode 100644 .vsts-pipelines/builds/ci-public.yml diff --git a/.vsts-pipelines/builds/ci-internal.yml b/.vsts-pipelines/builds/ci-internal.yml new file mode 100644 index 0000000000..d7ceb76378 --- /dev/null +++ b/.vsts-pipelines/builds/ci-internal.yml @@ -0,0 +1,13 @@ +trigger: +- dev +- release/* + +resources: + repositories: + - repository: buildtools + type: git + name: aspnet-BuildTools + ref: refs/heads/dev + +phases: +- template: .vsts-pipelines/templates/project-ci.yml@buildtools diff --git a/.vsts-pipelines/builds/ci-public.yml b/.vsts-pipelines/builds/ci-public.yml new file mode 100644 index 0000000000..bd08f3e4ea --- /dev/null +++ b/.vsts-pipelines/builds/ci-public.yml @@ -0,0 +1,28 @@ +trigger: +- dev +- release/* + +# See https://github.com/aspnet/BuildTools +resources: + repositories: + - repository: buildtools + type: github + endpoint: DotNet-Bot GitHub Connection + name: aspnet/BuildTools + ref: refs/heads/dev + +phases: +- template: .vsts-pipelines/templates/phases/default-build.yml@buildtools + parameters: + agentOs: Windows + beforeBuild: + - script: sqllocaldb start + displayName: Start LocalDB + +- template: .vsts-pipelines/templates/phases/default-build.yml@buildtools + parameters: + agentOs: macOS + +- template: .vsts-pipelines/templates/phases/default-build.yml@buildtools + parameters: + agentOs: Linux \ No newline at end of file From 7f463b27931f9614b13c94fc89df13d0b44e8712 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Mon, 25 Jun 2018 11:11:22 -0700 Subject: [PATCH 15/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 4c7a3f2e3c..8ac9bfab36 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,41 +3,41 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17081 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 - 2.2.0-preview1-34411 + 2.2.0-preview1-17090 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 + 2.2.0-preview1-34530 2.0.0 2.1.0 - 2.2.0-preview1-26606-01 + 2.2.0-preview1-26618-02 15.6.1 4.7.49 2.0.3 11.0.2 - 4.6.0-preview1-26605-01 - 1.7.0-preview1-26605-01 + 4.6.0-preview1-26617-01 + 1.7.0-preview1-26617-01 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index deb7e546f0..a8109db529 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17081 -commithash:73f09c256e2a54270951562ecc0ef4a953926c36 +version:2.2.0-preview1-17090 +commithash:b19e903e946579cd9482089bce7d917e8bacd765 From 2f8eaa5ea4960907dc71100502e08eb92508f8e6 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 28 Jun 2018 16:19:26 -0700 Subject: [PATCH 16/57] Update infrastructure for the 2.2 release --- .vsts-pipelines/builds/ci-internal.yml | 4 ++-- .vsts-pipelines/builds/ci-public.yml | 8 ++++---- build/repo.props | 1 + korebuild.json | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.vsts-pipelines/builds/ci-internal.yml b/.vsts-pipelines/builds/ci-internal.yml index d7ceb76378..dc7b8a3cb9 100644 --- a/.vsts-pipelines/builds/ci-internal.yml +++ b/.vsts-pipelines/builds/ci-internal.yml @@ -1,5 +1,5 @@ trigger: -- dev +- master - release/* resources: @@ -7,7 +7,7 @@ resources: - repository: buildtools type: git name: aspnet-BuildTools - ref: refs/heads/dev + ref: refs/heads/release/2.2 phases: - template: .vsts-pipelines/templates/project-ci.yml@buildtools diff --git a/.vsts-pipelines/builds/ci-public.yml b/.vsts-pipelines/builds/ci-public.yml index bd08f3e4ea..b37fbf760f 100644 --- a/.vsts-pipelines/builds/ci-public.yml +++ b/.vsts-pipelines/builds/ci-public.yml @@ -1,5 +1,5 @@ trigger: -- dev +- master - release/* # See https://github.com/aspnet/BuildTools @@ -9,8 +9,8 @@ resources: type: github endpoint: DotNet-Bot GitHub Connection name: aspnet/BuildTools - ref: refs/heads/dev - + ref: refs/heads/release/2.2 + phases: - template: .vsts-pipelines/templates/phases/default-build.yml@buildtools parameters: @@ -25,4 +25,4 @@ phases: - template: .vsts-pipelines/templates/phases/default-build.yml@buildtools parameters: - agentOs: Linux \ No newline at end of file + agentOs: Linux diff --git a/build/repo.props b/build/repo.props index 4a805f4722..22d6024b50 100644 --- a/build/repo.props +++ b/build/repo.props @@ -7,6 +7,7 @@ Internal.AspNetCore.Universe.Lineup + 2.2.0-* https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json diff --git a/korebuild.json b/korebuild.json index bd5d51a51b..d217d06e3e 100644 --- a/korebuild.json +++ b/korebuild.json @@ -1,4 +1,4 @@ { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev" + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.2/tools/korebuild.schema.json", + "channel": "release/2.2" } From a5ba2590d5760c84e5b043c15c46a6b81e323a75 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 8 Jul 2018 12:10:06 -0700 Subject: [PATCH 17/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 8ac9bfab36..7c6f8a270e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,34 +3,34 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17090 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.0.0 - 2.1.0 + 2.2.0-preview1-17099 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.2.0-preview1-34640 + 2.0.7 + 2.1.1 2.2.0-preview1-26618-02 15.6.1 4.7.49 @@ -39,7 +39,7 @@ 4.6.0-preview1-26617-01 1.7.0-preview1-26617-01 2.3.1 - 2.4.0-beta.1.build3945 + 2.4.0-rc.1.build4038 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index a8109db529..27e2e80f9a 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17090 -commithash:b19e903e946579cd9482089bce7d917e8bacd765 +version:2.2.0-preview1-17099 +commithash:263ed1db9866b6b419b1f5d5189a712aa218acb3 From d26d6ddfb136975687aaa975276efa3718c159d2 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 15 Jul 2018 12:10:02 -0700 Subject: [PATCH 18/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 7c6f8a270e..64954b8e73 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,40 +4,40 @@ 2.2.0-preview1-17099 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.2.0-preview1-34640 - 2.0.7 - 2.1.1 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.2.0-preview1-34694 + 2.0.9 + 2.1.2 2.2.0-preview1-26618-02 15.6.1 4.7.49 2.0.3 11.0.2 - 4.6.0-preview1-26617-01 - 1.7.0-preview1-26617-01 + 4.5.0 + 1.6.0 2.3.1 2.4.0-rc.1.build4038 From 4591b4172db1e02abf09eacefa55d23119e9825b Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 22 Jul 2018 12:09:40 -0700 Subject: [PATCH 19/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 50 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 64954b8e73..84ff308789 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,31 +4,31 @@ 2.2.0-preview1-17099 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 - 2.2.0-preview1-34694 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 + 2.2.0-preview1-34755 2.0.9 2.1.2 2.2.0-preview1-26618-02 From 1ace256ad04fe7066742fe45e3a1416634994c3f Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 29 Jul 2018 12:09:28 -0700 Subject: [PATCH 20/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 55 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 84ff308789..7ef83bd4d7 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,32 +3,32 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17099 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 + 2.2.0-preview1-17102 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 + 2.2.0-preview1-34823 2.0.9 2.1.2 2.2.0-preview1-26618-02 @@ -39,7 +39,8 @@ 4.5.0 1.6.0 2.3.1 - 2.4.0-rc.1.build4038 + 2.4.0 + diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 27e2e80f9a..6b8da29e6b 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17099 -commithash:263ed1db9866b6b419b1f5d5189a712aa218acb3 +version:2.2.0-preview1-17102 +commithash:e7e2b5a97ca92cfc6acc4def534cb0901a6d1eb9 From bf839dadf127ca0e8b10402722a9c73a7d30a9e6 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 31 Jul 2018 12:34:32 -0700 Subject: [PATCH 21/57] Delete ISSUE_TEMPLATE.md We're going to be doing some work in this repo. This issue tracker is the place to file Health Checks issues. --- .github/ISSUE_TEMPLATE.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 101a084f0a..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,3 +0,0 @@ -THIS ISSUE TRACKER IS CLOSED - please log new issues here: https://github.com/aspnet/Home/issues - -For information about this change, see https://github.com/aspnet/Announcements/issues/283 From 47f427d5ac6c3db6e09fd301a8646387319fcadc Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 31 Jul 2018 16:34:55 -0700 Subject: [PATCH 22/57] Updating Health Checks for 2.2 A bunch of small changes and updates for 2.2 focused at making our main scenarios more streamlined and focused. Also adds samples for extensibility we support so far. A list of changes: Clearing baselines for these projects. We didn't ship anything in 2.1 so there should be nothing in the baselines. -- The middleware now uses Map for path matching. This makes the actual `HealthCheckMiddleware` more standalone. This will make it easy to use with Dispatcher/Endpoint Routing in the future. This also manifests by removing Path from HealthCheckOptions - the path is an explicit argument to the UseHealthChecks middelware - this streamlines the design for 3.0. -- Added extensibility for customizing the status codes (aspnet/Home#2584) -- Added extensibility for writing the textual output (aspnet/Home#2583) -- Changed the default output to be `text/plain`. The most common use cases for health checks don't include a detailed status. The existing output format is still available as an option. --- build/dependencies.props | 1 + samples/HealthChecksSample/BasicStartup.cs | 35 ++ .../HealthChecksSample/CustomWriterStartup.cs | 67 ++++ .../DetailedStatusStartup.cs | 68 ++++ .../HealthChecksSample/GCInfoHealthCheck.cs | 41 ++ .../HealthChecksSample.csproj | 7 +- samples/HealthChecksSample/Program.cs | 37 +- samples/HealthChecksSample/Startup.cs | 28 -- ...HealthCheckApplicationBuilderExtensions.cs | 67 ++++ .../HealthCheckAppBuilderExtensions.cs | 32 -- .../HealthCheckMiddleware.cs | 88 ++--- .../HealthCheckOptions.cs | 22 +- .../HealthCheckResponseWriters.cs | 56 +++ .../baseline.netcore.json | 110 ------ .../baseline.netcore.json | 372 ------------------ .../HealthCheck.cs | 16 +- .../HealthChecksBuilderAddCheckExtensions.cs | 61 ++- .../baseline.netcore.json | 329 ---------------- .../HealthCheckMiddlewareSampleTest.cs | 62 +++ .../HealthCheckMiddlewareTests.cs | 289 +++++++++----- ...Core.Diagnostics.HealthChecks.Tests.csproj | 1 + 21 files changed, 763 insertions(+), 1026 deletions(-) create mode 100644 samples/HealthChecksSample/BasicStartup.cs create mode 100644 samples/HealthChecksSample/CustomWriterStartup.cs create mode 100644 samples/HealthChecksSample/DetailedStatusStartup.cs create mode 100644 samples/HealthChecksSample/GCInfoHealthCheck.cs delete mode 100644 samples/HealthChecksSample/Startup.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckAppBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs create mode 100644 test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs diff --git a/build/dependencies.props b/build/dependencies.props index 7ef83bd4d7..c201e62915 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -16,6 +16,7 @@ 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 + 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 diff --git a/samples/HealthChecksSample/BasicStartup.cs b/samples/HealthChecksSample/BasicStartup.cs new file mode 100644 index 0000000000..c89d78c8e5 --- /dev/null +++ b/samples/HealthChecksSample/BasicStartup.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace HealthChecksSample +{ + // Pass in `--scenario basic` at the command line to run this sample. + public class BasicStartup + { + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services.AddHealthChecks(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health. + // + // By default health checks will return a 200 with 'Healthy'. + // - No health checks are registered by default, the app is healthy if it is reachable + // - The default response writer writes the HealthCheckStatus as text/plain content + // + // This is the simplest way to use health checks, it is suitable for systems + // that want to check for 'liveness' of an application. + app.UseHealthChecks("/health"); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/CustomWriterStartup.cs b/samples/HealthChecksSample/CustomWriterStartup.cs new file mode 100644 index 0000000000..87e58b7b7d --- /dev/null +++ b/samples/HealthChecksSample/CustomWriterStartup.cs @@ -0,0 +1,67 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + // Pass in `--scenario writer` at the command line to run this sample. + public class CustomWriterStartup + { + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services.AddHealthChecks(); + + // This is an example of registering a custom health check as a service. + // All IHealthCheck services will be available to the health check service and + // middleware. + // + // We recommend registering all health checks as Singleton services. + services.AddSingleton(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health + // + // This example overrides the HealthCheckResponseWriter to write the health + // check result in a totally custom way. + app.UseHealthChecks("/health", new HealthCheckOptions() + { + // This custom writer formats the detailed status as an HTML table. + ResponseWriter = WriteResponse, + }); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status"); + }); + } + + private static Task WriteResponse(HttpContext httpContext, CompositeHealthCheckResult result) + { + httpContext.Response.ContentType = "text/html"; + return httpContext.Response.WriteAsync($@" + + +

+ Everything is {result.Status} +

+ + + + + + {string.Join("", result.Results.Select(kvp => $""))} + +
NameStatus
{kvp.Key}{kvp.Value.Status}
+ +"); + } + } +} diff --git a/samples/HealthChecksSample/DetailedStatusStartup.cs b/samples/HealthChecksSample/DetailedStatusStartup.cs new file mode 100644 index 0000000000..76475c5db5 --- /dev/null +++ b/samples/HealthChecksSample/DetailedStatusStartup.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + // Pass in `--scenario detailed` at the command line to run this sample. + public class DetailedStatusStartup + { + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services + .AddHealthChecks() + + // Registers a custom health check, in this case it will execute an + // inline delegate. + .AddCheck("GC Info", () => + { + // This example will report degraded status if the application is using + // more than 1gb of memory. + // + // Additionally we include some GC info in the reported diagnostics. + var allocated = GC.GetTotalMemory(forceFullCollection: false); + var data = new Dictionary() + { + { "Allocated", allocated }, + { "Gen0Collections", GC.CollectionCount(0) }, + { "Gen1Collections", GC.CollectionCount(1) }, + { "Gen2Collections", GC.CollectionCount(2) }, + }; + + // Report degraded status if the allocated memory is >= 1gb (in bytes) + var status = allocated >= 1024 * 1024 * 1024 ? HealthCheckStatus.Degraded : HealthCheckStatus.Healthy; + + return Task.FromResult(new HealthCheckResult( + status, + exception: null, + description: "reports degraded status if allocated bytes >= 1gb", + data: data)); + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health + // + // This example overrides the ResponseWriter to include a detailed + // status as JSON. Use this response writer (or create your own) to include + // detailed diagnostic information for use by a monitoring system. + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/GCInfoHealthCheck.cs b/samples/HealthChecksSample/GCInfoHealthCheck.cs new file mode 100644 index 0000000000..644fdb0a32 --- /dev/null +++ b/samples/HealthChecksSample/GCInfoHealthCheck.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + // This is an example of a custom health check that implements IHealthCheck. + // This is the same core logic as the DetailedStatusStartup example. + // See CustomWriterStartup to see how this is registered. + public class GCInfoHealthCheck : IHealthCheck + { + public string Name { get; } = "GCInfo"; + + public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + // This example will report degraded status if the application is using + // more than 1gb of memory. + // + // Additionally we include some GC info in the reported diagnostics. + var allocated = GC.GetTotalMemory(forceFullCollection: false); + var data = new Dictionary() + { + { "Allocated", allocated }, + { "Gen0Collections", GC.CollectionCount(0) }, + { "Gen1Collections", GC.CollectionCount(1) }, + { "Gen2Collections", GC.CollectionCount(2) }, + }; + + // Report degraded status if the allocated memory is >= 1gb (in bytes) + var status = allocated >= 1024 * 1024 * 1024 ? HealthCheckStatus.Degraded : HealthCheckStatus.Healthy; + + return Task.FromResult(new HealthCheckResult( + status, + exception: null, + description: "reports degraded status if allocated bytes >= 1gb", + data: data)); + } + } +} diff --git a/samples/HealthChecksSample/HealthChecksSample.csproj b/samples/HealthChecksSample/HealthChecksSample.csproj index 19ef79337e..2f3c0e6aaa 100644 --- a/samples/HealthChecksSample/HealthChecksSample.csproj +++ b/samples/HealthChecksSample/HealthChecksSample.csproj @@ -1,10 +1,13 @@ - + - netcoreapp2.0 + + netcoreapp2.0;net461 + netcoreapp2.0 + diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index be3f8bcdf1..bd72511db6 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -1,23 +1,54 @@ +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace HealthChecksSample { public class Program { + private static readonly Dictionary _scenarios; + + static Program() + { + _scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "", typeof(BasicStartup) }, + { "basic", typeof(BasicStartup) }, + { "detailed", typeof(DetailedStatusStartup) }, + { "writer", typeof(CustomWriterStartup) }, + }; + } + public static void Main(string[] args) { BuildWebHost(args).Run(); } - public static IWebHost BuildWebHost(string[] args) => - new WebHostBuilder() + public static IWebHost BuildWebHost(string[] args) + { + var config = new ConfigurationBuilder() + .AddEnvironmentVariables(prefix: "ASPNETCORE_") + .AddCommandLine(args) + .Build(); + + var scenario = config["scenario"] ?? string.Empty; + if (!_scenarios.TryGetValue(scenario, out var startupType)) + { + startupType = typeof(BasicStartup); + } + + return new WebHostBuilder() + .UseConfiguration(config) .ConfigureLogging(builder => { builder.AddConsole(); }) .UseKestrel() - .UseStartup() + .UseStartup(startupType) .Build(); + } + } } diff --git a/samples/HealthChecksSample/Startup.cs b/samples/HealthChecksSample/Startup.cs deleted file mode 100644 index e3bb04150c..0000000000 --- a/samples/HealthChecksSample/Startup.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; - -namespace HealthChecksSample -{ - public class Startup - { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddHealthChecks(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - app.UseHealthChecks("/health"); - - app.Run(async (context) => - { - await context.Response.WriteAsync("Hello World!"); - }); - } - } -} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..f1f47b3ba5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs @@ -0,0 +1,67 @@ +// 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. + +using System; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// extension methods for the . + /// + public static class HealthCheckApplicationBuilderExtensions + { + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// A reference to the after the operation has completed. + /// + /// The health check middleware will use default settings other than the provided . + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + return app.Map(path, b => b.UseMiddleware()); + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// A used to configure the middleware. + /// A reference to the after the operation has completed. + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, HealthCheckOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.Map(path, b => b.UseMiddleware(Options.Create(options))); + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckAppBuilderExtensions.cs deleted file mode 100644 index 916ca46df4..0000000000 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckAppBuilderExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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. - -using System; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Builder -{ - /// - /// extension methods for the . - /// - public static class HealthCheckAppBuilderExtensions - { - /// - /// Adds a middleware that provides a REST API for requesting health check status. - /// - /// The . - /// The path on which to provide the API. - /// A reference to the after the operation has completed. - public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path) - { - app = app ?? throw new ArgumentNullException(nameof(app)); - - return app.UseMiddleware(Options.Create(new HealthCheckOptions() - { - Path = path - })); - } - } -} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 18c10da287..5f3ca6df47 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -2,14 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { @@ -19,60 +15,62 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks private readonly HealthCheckOptions _healthCheckOptions; private readonly IHealthCheckService _healthCheckService; - public HealthCheckMiddleware(RequestDelegate next, IOptions healthCheckOptions, IHealthCheckService healthCheckService) + public HealthCheckMiddleware( + RequestDelegate next, + IOptions healthCheckOptions, + IHealthCheckService healthCheckService) { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (healthCheckOptions == null) + { + throw new ArgumentNullException(nameof(healthCheckOptions)); + } + + if (healthCheckService == null) + { + throw new ArgumentNullException(nameof(healthCheckService)); + } + _next = next; _healthCheckOptions = healthCheckOptions.Value; _healthCheckService = healthCheckService; } /// - /// Process an individual request. + /// Processes a request. /// - /// + /// /// - public async Task InvokeAsync(HttpContext context) + public async Task InvokeAsync(HttpContext httpContext) { - if (context.Request.Path == _healthCheckOptions.Path) + if (httpContext == null) { - // Get results - var result = await _healthCheckService.CheckHealthAsync(context.RequestAborted); - - // Map status to response code - switch (result.Status) - { - case HealthCheckStatus.Failed: - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - break; - case HealthCheckStatus.Unhealthy: - context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - break; - case HealthCheckStatus.Degraded: - // Degraded doesn't mean unhealthy so we return 200, but the content will contain more details - context.Response.StatusCode = StatusCodes.Status200OK; - break; - case HealthCheckStatus.Healthy: - context.Response.StatusCode = StatusCodes.Status200OK; - break; - default: - // This will only happen when we change HealthCheckStatus and we don't update this. - Debug.Fail($"Unrecognized HealthCheckStatus value: {result.Status}"); - throw new InvalidOperationException($"Unrecognized HealthCheckStatus value: {result.Status}"); - } - - // Render results to JSON - var json = new JObject( - new JProperty("status", result.Status.ToString()), - new JProperty("results", new JObject(result.Results.Select(pair => - new JProperty(pair.Key, new JObject( - new JProperty("status", pair.Value.Status.ToString()), - new JProperty("description", pair.Value.Description), - new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value)))))))))); - await context.Response.WriteAsync(json.ToString(Formatting.None)); + throw new ArgumentNullException(nameof(httpContext)); } - else + + // Get results + var result = await _healthCheckService.CheckHealthAsync(httpContext.RequestAborted); + + // Map status to response code - this is customizable via options. + if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) { - await _next(context); + var message = + $"No status code mapping found for {nameof(HealthCheckStatus)} value: {result.Status}." + + $"{nameof(HealthCheckOptions)}.{nameof(HealthCheckOptions.ResultStatusCodes)} must contain" + + $"an entry for {result.Status}."; + + throw new InvalidOperationException(message); + } + + httpContext.Response.StatusCode = statusCode; + + if (_healthCheckOptions.ResponseWriter != null) + { + await _healthCheckOptions.ResponseWriter(httpContext, result); } } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 8416e6843d..6426fcbc3b 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -1,7 +1,11 @@ // 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. +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { @@ -10,9 +14,23 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// public class HealthCheckOptions { + public IDictionary ResultStatusCodes { get; } = new Dictionary() + { + { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, + { HealthCheckStatus.Degraded, StatusCodes.Status200OK }, + { HealthCheckStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, + + // This means that a health check failed, so 500 is appropriate. This is an error. + { HealthCheckStatus.Failed, StatusCodes.Status500InternalServerError }, + }; + /// - /// Gets or sets the path at which the Health Check results will be available. + /// Gets or sets a delegate used to write the response. /// - public PathString Path { get; set; } + /// + /// The default value is a delegate that will write a minimal text/plain response with the value + /// of as a string. + /// + public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs new file mode 100644 index 0000000000..95ddf00ac8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs @@ -0,0 +1,56 @@ +// 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. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Diagnostics.HealthChecks +{ + public static class HealthCheckResponseWriters + { + public static Task WriteMinimalPlaintext(HttpContext httpContext, CompositeHealthCheckResult result) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + httpContext.Response.ContentType = "text/plain"; + return httpContext.Response.WriteAsync(result.Status.ToString()); + } + + public static Task WriteDetailedJson(HttpContext httpContext, CompositeHealthCheckResult result) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + httpContext.Response.ContentType = "application/json"; + + var json = new JObject( + new JProperty("status", result.Status.ToString()), + new JProperty("results", new JObject(result.Results.Select(pair => + new JProperty(pair.Key, new JObject( + new JProperty("status", pair.Value.Status.ToString()), + new JProperty("description", pair.Value.Description), + new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value)))))))))); + return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json index c0e8deddd1..d089d2470d 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/baseline.netcore.json @@ -1,115 +1,5 @@ { "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ - { - "Name": "Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "InvokeAsync", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Http.HttpContext" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "next", - "Type": "Microsoft.AspNetCore.Http.RequestDelegate" - }, - { - "Name": "healthCheckOptions", - "Type": "Microsoft.Extensions.Options.IOptions" - }, - { - "Name": "healthCheckService", - "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Path", - "Parameters": [], - "ReturnType": "Microsoft.AspNetCore.Http.PathString", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_Path", - "Parameters": [ - { - "Name": "value", - "Type": "Microsoft.AspNetCore.Http.PathString" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.AspNetCore.Builder.HealthCheckAppBuilderExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "UseHealthChecks", - "Parameters": [ - { - "Name": "app", - "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" - }, - { - "Name": "path", - "Type": "Microsoft.AspNetCore.Http.PathString" - } - ], - "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } ] } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json index 7792748d55..871db4c089 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/baseline.netcore.json @@ -1,377 +1,5 @@ { "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Visibility": "Public", - "Kind": "Struct", - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Status", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Exception", - "Parameters": [], - "ReturnType": "System.Exception", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Description", - "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Data", - "Parameters": [], - "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "exception", - "Type": "System.Exception" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "exception", - "Type": "System.Exception" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Unhealthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "exception", - "Type": "System.Exception" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Healthy", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Healthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Healthy", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "exception", - "Type": "System.Exception" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "exception", - "Type": "System.Exception" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Degraded", - "Parameters": [ - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "exception", - "Type": "System.Exception" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "status", - "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus" - }, - { - "Name": "exception", - "Type": "System.Exception" - }, - { - "Name": "description", - "Type": "System.String" - }, - { - "Name": "data", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", - "Visibility": "Public", - "Kind": "Enumeration", - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Field", - "Name": "Unknown", - "Parameters": [], - "GenericParameter": [], - "Literal": "0" - }, - { - "Kind": "Field", - "Name": "Failed", - "Parameters": [], - "GenericParameter": [], - "Literal": "1" - }, - { - "Kind": "Field", - "Name": "Unhealthy", - "Parameters": [], - "GenericParameter": [], - "Literal": "2" - }, - { - "Kind": "Field", - "Name": "Degraded", - "Parameters": [], - "GenericParameter": [], - "Literal": "3" - }, - { - "Kind": "Field", - "Name": "Healthy", - "Parameters": [], - "GenericParameter": [], - "Literal": "4" - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Name", - "Parameters": [], - "ReturnType": "System.String", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } ] } \ No newline at end of file diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs index e72910df33..da6a92d062 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs @@ -11,15 +11,10 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// A simple implementation of which uses a provided delegate to /// implement the check. /// - public class HealthCheck : IHealthCheck + public sealed class HealthCheck : IHealthCheck { private readonly Func> _check; - /// - /// Gets the name of the health check, which should indicate the component being checked. - /// - public string Name { get; } - /// /// Create an instance of from the specified and . /// @@ -27,10 +22,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// A delegate which provides the code to execute when the health check is run. public HealthCheck(string name, Func> check) { - Name = name; - _check = check; + Name = name ?? throw new ArgumentNullException(nameof(name)); + _check = check ?? throw new ArgumentNullException(nameof(check)); } + /// + /// Gets the name of the health check, which should indicate the component being checked. + /// + public string Name { get; } + /// /// Runs the health check, returning the status of the component being checked. /// diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs index 3b3ee1eb54..b2f931fcd3 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs @@ -22,8 +22,22 @@ namespace Microsoft.Extensions.DependencyInjection /// The . public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) { - builder.Services.AddSingleton(services => new HealthCheck(name, check)); - return builder; + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + return builder.AddCheck(new HealthCheck(name, check)); } /// @@ -33,7 +47,46 @@ namespace Microsoft.Extensions.DependencyInjection /// The name of the health check, which should indicate the component being checked. /// A delegate which provides the code to execute when the health check is run. /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) => - builder.AddCheck(name, _ => check()); + public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + return builder.AddCheck(name, _ => check()); + } + + /// + /// Adds a new health check with the implementation. + /// + /// The to add the check to. + /// An implementation. + /// The . + public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, IHealthCheck check) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + builder.Services.AddSingleton(check); + return builder; + } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json b/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json index 4938a12605..cb2fe053f1 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/baseline.netcore.json @@ -1,334 +1,5 @@ { "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ - { - "Name": "Microsoft.Extensions.DependencyInjection.HealthChecksBuilderAddCheckExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "AddCheck", - "Parameters": [ - { - "Name": "builder", - "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder" - }, - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "check", - "Type": "System.Func>" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "AddCheck", - "Parameters": [ - { - "Name": "builder", - "Type": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder" - }, - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "check", - "Type": "System.Func>" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.DependencyInjection.HealthCheckServiceCollectionExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "AddHealthChecks", - "Parameters": [ - { - "Name": "services", - "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" - } - ], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.CompositeHealthCheckResult", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Results", - "Parameters": [], - "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Status", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckStatus", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "results", - "Type": "System.Collections.Generic.IReadOnlyDictionary" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheck", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck" - ], - "Members": [ - { - "Kind": "Method", - "Name": "get_Name", - "Parameters": [], - "ReturnType": "System.String", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "check", - "Type": "System.Func>" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService" - ], - "Members": [ - { - "Kind": "Method", - "Name": "get_Checks", - "Parameters": [], - "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "checks", - "Type": "System.Collections.Generic.IEnumerable" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "healthChecks", - "Type": "System.Collections.Generic.IEnumerable" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "healthChecks", - "Type": "System.Collections.Generic.IEnumerable" - }, - { - "Name": "logger", - "Type": "Microsoft.Extensions.Logging.ILogger" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Services", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Checks", - "Parameters": [], - "ReturnType": "System.Collections.Generic.IReadOnlyDictionary", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CheckHealthAsync", - "Parameters": [ - { - "Name": "checks", - "Type": "System.Collections.Generic.IEnumerable" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } ] } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs new file mode 100644 index 0000000000..1007017e11 --- /dev/null +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs @@ -0,0 +1,62 @@ +// 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. + + +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Microsoft.AspNetCore.Diagnostics.HealthChecks +{ + public class HealthCheckMiddlewareSampleTest + { + [Fact] + public async Task BasicStartup() + { + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CustomWriterStartup() + { + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/html", response.Content.Headers.ContentType.ToString()); + + // Ignoring the body since it contains a bunch of statistics + } + + [Fact] + public async Task DetailedStatusStartup() + { + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + + // Ignoring the body since it contains a bunch of statistics + } + } +} diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index c6aaee52ff..b81747581c 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -17,10 +17,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { public class HealthCheckMiddlewareTests { - [Theory] - [InlineData("/frob")] - [InlineData("/health/")] // Match is exact, for now at least - public async Task IgnoresRequestThatDoesNotMatchPath(string requestPath) + [Fact] // Matches based on '.Map' + public async Task IgnoresRequestThatDoesNotMatchPath() { var builder = new WebHostBuilder() .Configure(app => @@ -34,26 +32,190 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks var server = new TestServer(builder); var client = server.CreateClient(); - var response = await client.GetAsync(requestPath); + var response = await client.GetAsync("/frob"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Theory] - [InlineData("/health")] - [InlineData("/Health")] - [InlineData("/HEALTH")] - public async Task ReturnsEmptyHealthyRequestIfNoHealthChecksRegistered(string requestPath) + [Fact] // Matches based on '.Map' + public async Task MatchIsCaseInsensitive() + { + 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); + } + + [Fact] + public async Task ReturnsPlainTextStatus() + { + 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("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task StatusCodeIs200IfNoChecks() + { + 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("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + + [Fact] + public async Task StatusCodeIs200IfAllChecksHealthy() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task StatusCodeIs200IfCheckIsDegraded() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Not so great."))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Degraded", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task StatusCodeIs503IfCheckIsUnhealthy() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task StatusCodeIs500IfCheckIsFailed() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddCheck("Bar", () => Task.FromResult(new HealthCheckResult(HealthCheckStatus.Failed, null, null, null))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Failed", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task DetailedJsonReturnsEmptyHealthyResponseIfNoHealthChecksRegistered() { var expectedJson = JsonConvert.SerializeObject(new { status = "Healthy", results = new { } - }, Formatting.None); + }, Formatting.Indented); var builder = new WebHostBuilder() .Configure(app => { - app.UseHealthChecks("/health"); + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); }) .ConfigureServices(services => { @@ -62,14 +224,14 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks var server = new TestServer(builder); var client = server.CreateClient(); - var response = await client.GetAsync(requestPath); + var response = await client.GetAsync("/health"); var result = await response.Content.ReadAsStringAsync(); Assert.Equal(expectedJson, result); } [Fact] - public async Task ReturnsResultsFromHealthChecks() + public async Task DetailedJsonReturnsResultsFromHealthChecks() { var expectedJson = JsonConvert.SerializeObject(new { @@ -101,12 +263,15 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks data = new { } }, }, - }, Formatting.None); + }, Formatting.Indented); var builder = new WebHostBuilder() .Configure(app => { - app.UseHealthChecks("/health"); + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); }) .ConfigureServices(services => { @@ -129,78 +294,15 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks } [Fact] - public async Task StatusCodeIs200IfNoChecks() + public async Task NoResponseWriterReturnsEmptyBody() { 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); - } - - [Fact] - public async Task StatusCodeIs200IfAllChecksHealthy() - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseHealthChecks("/health"); - }) - .ConfigureServices(services => - { - services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); - }); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var response = await client.GetAsync("/health"); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task StatusCodeIs200IfCheckIsDegraded() - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseHealthChecks("/health"); - }) - .ConfigureServices(services => - { - services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Not so great."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); - }); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var response = await client.GetAsync("/health"); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task StatusCodeIs503IfCheckIsUnhealthy() - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseHealthChecks("/health"); + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = null, + }); }) .ConfigureServices(services => { @@ -215,29 +317,34 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); } [Fact] - public async Task StatusCodeIs500IfCheckIsFailed() + public async Task CanSetCustomStatusCodes() { var builder = new WebHostBuilder() .Configure(app => { - app.UseHealthChecks("/health"); + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResultStatusCodes = + { + [HealthCheckStatus.Healthy] = 201, + } + }); }) .ConfigureServices(services => { - services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(new HealthCheckResult(HealthCheckStatus.Failed, null, null, null))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + services.AddHealthChecks(); }); var server = new TestServer(builder); var client = server.CreateClient(); var response = await client.GetAsync("/health"); - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj index 001b0c0990..8ac9320ae8 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj @@ -14,6 +14,7 @@ + From 64124e9c8591dccef583a0e004414a8dfe6df5d6 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 31 Jul 2018 18:00:51 -0700 Subject: [PATCH 23/57] Add filtering to Health Checks middleware This allows each middleware to be configured with a specific set of checks (by name). See the comments in the sample for how this is frequently used. This is also addresses aspnet/Home#2575 - or at least the part that we plan to do. We think that any sort of built-in system of metadata or tags is vast overkill, and doesn't really align with the primary usage of health checks. We're providing building blocks, and these can be put together to build more complicated things like what's described in aspnet/Home#2575. --- .../LivenessProbeStartup.cs | 80 +++++++++++++++++++ samples/HealthChecksSample/Program.cs | 1 + .../SlowDependencyHealthCheck.cs | 32 ++++++++ .../HealthCheckMiddleware.cs | 43 +++++++++- .../HealthCheckOptions.cs | 10 +++ .../HealthCheckMiddlewareSampleTest.cs | 65 +++++++++++++++ .../HealthCheckMiddlewareTests.cs | 62 ++++++++++++++ 7 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 samples/HealthChecksSample/LivenessProbeStartup.cs create mode 100644 samples/HealthChecksSample/SlowDependencyHealthCheck.cs diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs new file mode 100644 index 0000000000..d7645b5f0e --- /dev/null +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + // Pass in `--scenario liveness` at the command line to run this sample. + public class LivenessProbeStartup + { + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services + .AddHealthChecks() + .AddCheck("identity", () => Task.FromResult(HealthCheckResult.Healthy())) + .AddCheck(new SlowDependencyHealthCheck()); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware twice: + // - at /health/ready for 'readiness' + // - at /health/live for 'liveness' + // + // Using a separate liveness and readiness check is useful in an environment like Kubernetes + // when an application needs to do significant work before accepting requests. Using separate + // checks allows the orchestrator to distinguish whether the application is functioning but + // not yet ready or if the application has failed to start. + // + // For instance the liveness check will do a quick set of checks to determine if the process + // is functioning correctly. + // + // The readiness check might do a set of more expensive or time-consuming checks to determine + // if all other resources are responding. + // + // See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ for + // more details about readiness and liveness probes in Kubernetes. + // + // In this example, the liveness check will us an 'identity' check that always returns healthy. + // + // In this example, the readiness check will run all registered checks, include a check with an + // long initialization time (15 seconds). + + + // The readiness check uses all of the registered health checks (default) + app.UseHealthChecks("/health/ready", new HealthCheckOptions() + { + // This sample is using detailed status to make more apparent which checks are being run - any + // output format will work with liveness and readiness checks. + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); + + // The liveness check uses an 'identity' health check that always returns healty + app.UseHealthChecks("/health/live", new HealthCheckOptions() + { + // Filters the set of health checks run by this middleware + HealthCheckNames = + { + "identity", + }, + + // This sample is using detailed status to make more apparent which checks are being run - any + // output format will work with liveness and readiness checks. + ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + }); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health/ready to see the readiness status"); + await context.Response.WriteAsync(Environment.NewLine); + await context.Response.WriteAsync("Go to /health/live to see the liveness status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index bd72511db6..4d9455ed2d 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -18,6 +18,7 @@ namespace HealthChecksSample { "basic", typeof(BasicStartup) }, { "detailed", typeof(DetailedStatusStartup) }, { "writer", typeof(CustomWriterStartup) }, + { "liveness", typeof(LivenessProbeStartup) }, }; } diff --git a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs new file mode 100644 index 0000000000..1ca03f88ae --- /dev/null +++ b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + // Simulates a health check for an application dependency that takes a while to initialize. + // This is part of the readiness/liveness probe sample. + public class SlowDependencyHealthCheck : IHealthCheck + { + public static readonly string HealthCheckName = "slow_dependency"; + + private readonly Task _task; + + public SlowDependencyHealthCheck() + { + _task = Task.Delay(15 * 1000); + } + + public string Name => HealthCheckName; + + public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + if (_task.IsCompleted) + { + return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready")); + } + + return Task.FromResult(HealthCheckResult.Unhealthy("Dependency is still initializing")); + } + } +} diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 5f3ca6df47..3a220f2595 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks private readonly RequestDelegate _next; private readonly HealthCheckOptions _healthCheckOptions; private readonly IHealthCheckService _healthCheckService; + private readonly IHealthCheck[] _checks; public HealthCheckMiddleware( RequestDelegate next, @@ -38,6 +41,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks _next = next; _healthCheckOptions = healthCheckOptions.Value; _healthCheckService = healthCheckService; + + _checks = FilterHealthChecks(_healthCheckService.Checks, healthCheckOptions.Value.HealthCheckNames); } /// @@ -53,7 +58,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks } // Get results - var result = await _healthCheckService.CheckHealthAsync(httpContext.RequestAborted); + var result = await _healthCheckService.CheckHealthAsync(_checks, httpContext.RequestAborted); // Map status to response code - this is customizable via options. if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) @@ -73,5 +78,41 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks await _healthCheckOptions.ResponseWriter(httpContext, result); } } + + private static IHealthCheck[] FilterHealthChecks( + IReadOnlyDictionary checks, + ISet names) + { + // If there are no filters then include all checks. + if (names.Count == 0) + { + return checks.Values.ToArray(); + } + + // Keep track of what we don't find so we can report errors. + var notFound = new HashSet(names, StringComparer.OrdinalIgnoreCase); + var matches = new List(); + + foreach (var kvp in checks) + { + if (!notFound.Remove(kvp.Key)) + { + // This check was excluded + continue; + } + + matches.Add(kvp.Value); + } + + if (notFound.Count > 0) + { + var message = + $"The following health checks were not found: '{string.Join(", ", notFound)}'. " + + $"Registered health checks: '{string.Join(", ", checks.Keys)}'."; + throw new InvalidOperationException(message); + } + + return matches.ToArray(); + } } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 6426fcbc3b..b57374f2ae 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -14,6 +14,16 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// public class HealthCheckOptions { + /// + /// Gets a set of health check names used to filter the set of health checks run. + /// + /// + /// If is empty, the will run all + /// registered health checks - this is the default behavior. To run a subset of health checks, + /// add the names of the desired health checks. + /// + public ISet HealthCheckNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); + public IDictionary ResultStatusCodes { get; } = new Dictionary() { { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs index 1007017e11..4e716ed3b9 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs @@ -6,6 +6,7 @@ using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; using Xunit; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks @@ -58,5 +59,69 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks // Ignoring the body since it contains a bunch of statistics } + + [Fact] + public async Task LivenessProbeStartup_Liveness() + { + var expectedJson = JsonConvert.SerializeObject(new + { + status = "Healthy", + results = new + { + identity = new + { + status = "Healthy", + description = "", + data = new { } + }, + }, + }, Formatting.Indented); + + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health/live"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.Equal(expectedJson, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task LivenessProbeStartup_Readiness() + { + var expectedJson = JsonConvert.SerializeObject(new + { + status = "Unhealthy", + results = new + { + identity = new + { + status = "Healthy", + description = "", + data = new { } + }, + slow_dependency = new + { + status = "Unhealthy", + description = "Dependency is still initializing", + data = new { } + }, + }, + }, Formatting.Indented); + + var builder = new WebHostBuilder() + .UseStartup(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health/ready"); + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.Equal(expectedJson, await response.Content.ReadAsStringAsync()); + } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index b81747581c..74d23c81bf 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -346,5 +346,67 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } + + [Fact] + public async Task CanFilterChecks() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + HealthCheckNames = + { + "Baz", + "FOO", + }, + }); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + // Will get filtered out + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public void CanFilterChecks_ThrowsForMissingCheck() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions() + { + HealthCheckNames = + { + "Bazzzzzz", + "FOO", + }, + }); + }) + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + }); + + var ex = Assert.Throws(() => new TestServer(builder)); + Assert.Equal( + "The following health checks were not found: 'Bazzzzzz'. Registered health checks: 'Foo, Bar, Baz'.", + ex.Message); + } } } From 4549b84cb563de5e173849cf920498c3bdb735a1 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 3 Aug 2018 09:40:45 -0700 Subject: [PATCH 24/57] Remove JSON output (#457) * Remove JSON output and convert to sample GlennC and I made the decisison to turn the JSON output into a sample rather than something we support out of the box. We wouldn't tell customers to definitely use it and we don't want to introduce more coupling to JSON.NET. --- .../HealthChecksSample/CustomWriterStartup.cs | 29 +++---- .../DetailedStatusStartup.cs | 68 ---------------- .../HealthChecksSample/GCInfoHealthCheck.cs | 2 +- .../HealthChecksSample.csproj | 1 + .../LivenessProbeStartup.cs | 11 +-- samples/HealthChecksSample/Program.cs | 1 - ...HealthCheckApplicationBuilderExtensions.cs | 14 +++- .../HealthCheckResponseWriters.cs | 40 +-------- ...AspNetCore.Diagnostics.HealthChecks.csproj | 3 +- .../HealthCheckMiddlewareSampleTest.cs | 60 +------------- .../HealthCheckMiddlewareTests.cs | 81 ++++--------------- 11 files changed, 48 insertions(+), 262 deletions(-) delete mode 100644 samples/HealthChecksSample/DetailedStatusStartup.cs diff --git a/samples/HealthChecksSample/CustomWriterStartup.cs b/samples/HealthChecksSample/CustomWriterStartup.cs index 87e58b7b7d..97003068fc 100644 --- a/samples/HealthChecksSample/CustomWriterStartup.cs +++ b/samples/HealthChecksSample/CustomWriterStartup.cs @@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace HealthChecksSample { @@ -45,23 +47,16 @@ namespace HealthChecksSample private static Task WriteResponse(HttpContext httpContext, CompositeHealthCheckResult result) { - httpContext.Response.ContentType = "text/html"; - return httpContext.Response.WriteAsync($@" - - -

- Everything is {result.Status} -

- - - - - - {string.Join("", result.Results.Select(kvp => $""))} - -
NameStatus
{kvp.Key}{kvp.Value.Status}
- -"); + httpContext.Response.ContentType = "application/json"; + + var json = new JObject( + new JProperty("status", result.Status.ToString()), + new JProperty("results", new JObject(result.Results.Select(pair => + new JProperty(pair.Key, new JObject( + new JProperty("status", pair.Value.Status.ToString()), + new JProperty("description", pair.Value.Description), + new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value)))))))))); + return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented)); } } } diff --git a/samples/HealthChecksSample/DetailedStatusStartup.cs b/samples/HealthChecksSample/DetailedStatusStartup.cs deleted file mode 100644 index 76475c5db5..0000000000 --- a/samples/HealthChecksSample/DetailedStatusStartup.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace HealthChecksSample -{ - // Pass in `--scenario detailed` at the command line to run this sample. - public class DetailedStatusStartup - { - public void ConfigureServices(IServiceCollection services) - { - // Registers required services for health checks - services - .AddHealthChecks() - - // Registers a custom health check, in this case it will execute an - // inline delegate. - .AddCheck("GC Info", () => - { - // This example will report degraded status if the application is using - // more than 1gb of memory. - // - // Additionally we include some GC info in the reported diagnostics. - var allocated = GC.GetTotalMemory(forceFullCollection: false); - var data = new Dictionary() - { - { "Allocated", allocated }, - { "Gen0Collections", GC.CollectionCount(0) }, - { "Gen1Collections", GC.CollectionCount(1) }, - { "Gen2Collections", GC.CollectionCount(2) }, - }; - - // Report degraded status if the allocated memory is >= 1gb (in bytes) - var status = allocated >= 1024 * 1024 * 1024 ? HealthCheckStatus.Degraded : HealthCheckStatus.Healthy; - - return Task.FromResult(new HealthCheckResult( - status, - exception: null, - description: "reports degraded status if allocated bytes >= 1gb", - data: data)); - }); - } - - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - // This will register the health checks middleware at the URL /health - // - // This example overrides the ResponseWriter to include a detailed - // status as JSON. Use this response writer (or create your own) to include - // detailed diagnostic information for use by a monitoring system. - app.UseHealthChecks("/health", new HealthCheckOptions() - { - ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, - }); - - app.Run(async (context) => - { - await context.Response.WriteAsync("Go to /health to see the health status"); - }); - } - } -} diff --git a/samples/HealthChecksSample/GCInfoHealthCheck.cs b/samples/HealthChecksSample/GCInfoHealthCheck.cs index 644fdb0a32..f37ade8980 100644 --- a/samples/HealthChecksSample/GCInfoHealthCheck.cs +++ b/samples/HealthChecksSample/GCInfoHealthCheck.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthChecksSample { // This is an example of a custom health check that implements IHealthCheck. - // This is the same core logic as the DetailedStatusStartup example. + // // See CustomWriterStartup to see how this is registered. public class GCInfoHealthCheck : IHealthCheck { diff --git a/samples/HealthChecksSample/HealthChecksSample.csproj b/samples/HealthChecksSample/HealthChecksSample.csproj index 2f3c0e6aaa..c8a2fded73 100644 --- a/samples/HealthChecksSample/HealthChecksSample.csproj +++ b/samples/HealthChecksSample/HealthChecksSample.csproj @@ -11,6 +11,7 @@ + diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs index d7645b5f0e..3a4078759f 100644 --- a/samples/HealthChecksSample/LivenessProbeStartup.cs +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -48,12 +48,7 @@ namespace HealthChecksSample // The readiness check uses all of the registered health checks (default) - app.UseHealthChecks("/health/ready", new HealthCheckOptions() - { - // This sample is using detailed status to make more apparent which checks are being run - any - // output format will work with liveness and readiness checks. - ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, - }); + app.UseHealthChecks("/health/ready"); // The liveness check uses an 'identity' health check that always returns healty app.UseHealthChecks("/health/live", new HealthCheckOptions() @@ -63,10 +58,6 @@ namespace HealthChecksSample { "identity", }, - - // This sample is using detailed status to make more apparent which checks are being run - any - // output format will work with liveness and readiness checks. - ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, }); app.Run(async (context) => diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index 4d9455ed2d..b4e8874f22 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -16,7 +16,6 @@ namespace HealthChecksSample { { "", typeof(BasicStartup) }, { "basic", typeof(BasicStartup) }, - { "detailed", typeof(DetailedStatusStartup) }, { "writer", typeof(CustomWriterStartup) }, { "liveness", typeof(LivenessProbeStartup) }, }; diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs index f1f47b3ba5..e6239a3488 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs @@ -20,7 +20,13 @@ namespace Microsoft.AspNetCore.Builder /// The path on which to provide health check status. /// A reference to the after the operation has completed. /// - /// The health check middleware will use default settings other than the provided . + /// + /// This method will use to + /// listen to health checks requests on the specified URL path. + /// + /// + /// The health check middleware will use default settings from . + /// /// public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path) { @@ -44,6 +50,12 @@ namespace Microsoft.AspNetCore.Builder /// The path on which to provide health check status. /// A used to configure the middleware. /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path. + /// + /// public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, HealthCheckOptions options) { if (app == null) diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs index 95ddf00ac8..a74072f63f 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs @@ -1,56 +1,18 @@ // 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. -using System; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { - public static class HealthCheckResponseWriters + internal static class HealthCheckResponseWriters { public static Task WriteMinimalPlaintext(HttpContext httpContext, CompositeHealthCheckResult result) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - httpContext.Response.ContentType = "text/plain"; return httpContext.Response.WriteAsync(result.Status.ToString()); } - - public static Task WriteDetailedJson(HttpContext httpContext, CompositeHealthCheckResult result) - { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - - httpContext.Response.ContentType = "application/json"; - - var json = new JObject( - new JProperty("status", result.Status.ToString()), - new JProperty("results", new JObject(result.Results.Select(pair => - new JProperty(pair.Key, new JObject( - new JProperty("status", pair.Value.Status.ToString()), - new JProperty("description", pair.Value.Description), - new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value)))))))))); - return httpContext.Response.WriteAsync(json.ToString(Formatting.Indented)); - } } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj index 76a24a4c47..b7dd85d4a6 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core middleware for returning the results of Health Checks in the application @@ -16,7 +16,6 @@ - diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs index 4e716ed3b9..bffed9ed66 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareSampleTest.cs @@ -1,12 +1,10 @@ // 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. - using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; using Xunit; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks @@ -37,22 +35,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks var server = new TestServer(builder); var client = server.CreateClient(); - var response = await client.GetAsync("/health"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("text/html", response.Content.Headers.ContentType.ToString()); - - // Ignoring the body since it contains a bunch of statistics - } - - [Fact] - public async Task DetailedStatusStartup() - { - var builder = new WebHostBuilder() - .UseStartup(); - - var server = new TestServer(builder); - var client = server.CreateClient(); - var response = await client.GetAsync("/health"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); @@ -63,20 +45,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks [Fact] public async Task LivenessProbeStartup_Liveness() { - var expectedJson = JsonConvert.SerializeObject(new - { - status = "Healthy", - results = new - { - identity = new - { - status = "Healthy", - description = "", - data = new { } - }, - }, - }, Formatting.Indented); - var builder = new WebHostBuilder() .UseStartup(); @@ -85,33 +53,13 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks var response = await client.GetAsync("/health/live"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); - Assert.Equal(expectedJson, await response.Content.ReadAsStringAsync()); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } [Fact] public async Task LivenessProbeStartup_Readiness() { - var expectedJson = JsonConvert.SerializeObject(new - { - status = "Unhealthy", - results = new - { - identity = new - { - status = "Healthy", - description = "", - data = new { } - }, - slow_dependency = new - { - status = "Unhealthy", - description = "Dependency is still initializing", - data = new { } - }, - }, - }, Formatting.Indented); - var builder = new WebHostBuilder() .UseStartup(); @@ -120,8 +68,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks var response = await client.GetAsync("/health/ready"); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); - Assert.Equal(expectedJson, await response.Content.ReadAsStringAsync()); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync()); } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 74d23c81bf..0555ab6f7a 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -7,6 +7,7 @@ using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -201,94 +202,40 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks } [Fact] - public async Task DetailedJsonReturnsEmptyHealthyResponseIfNoHealthChecksRegistered() - { - var expectedJson = JsonConvert.SerializeObject(new - { - status = "Healthy", - results = new { } - }, Formatting.Indented); - - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseHealthChecks("/health", new HealthCheckOptions() - { - ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, - }); - }) - .ConfigureServices(services => - { - services.AddHealthChecks(); - }); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var response = await client.GetAsync("/health"); - - var result = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedJson, result); - } - - [Fact] - public async Task DetailedJsonReturnsResultsFromHealthChecks() + public async Task CanUseCustomWriter() { var expectedJson = JsonConvert.SerializeObject(new { status = "Unhealthy", - results = new - { - Foo = new - { - status = "Healthy", - description = "Good to go!", - data = new { } - }, - Bar = new - { - status = "Degraded", - description = "Feeling a bit off.", - data = new { someUsefulAttribute = 42 } - }, - Baz = new - { - status = "Unhealthy", - description = "Not feeling good at all", - data = new { } - }, - Boz = new - { - status = "Unhealthy", - description = string.Empty, - data = new { } - }, - }, - }, Formatting.Indented); + }); var builder = new WebHostBuilder() .Configure(app => { app.UseHealthChecks("/health", new HealthCheckOptions() { - ResponseWriter = HealthCheckResponseWriters.WriteDetailedJson, + ResponseWriter = (c, r) => + { + var json = JsonConvert.SerializeObject(new { status = r.Status.ToString(), }); + c.Response.ContentType = "application/json"; + return c.Response.WriteAsync(json); + }, }); }) .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("Good to go!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Feeling a bit off.", new Dictionary() - { - { "someUsefulAttribute", 42 } - }))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Unhealthy("Not feeling good at all", new Exception("Bad times")))) - .AddCheck("Boz", () => Task.FromResult(HealthCheckResult.Unhealthy(new Exception("Very bad times")))); + .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) + .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); var response = await client.GetAsync("/health"); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + var result = await response.Content.ReadAsStringAsync(); Assert.Equal(expectedJson, result); } From ebafbcdae33ddb2981f96c7790e1c6f422a59a8a Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 3 Aug 2018 14:35:54 -0700 Subject: [PATCH 25/57] Add filtering by port This adds UseHealthChecks overloads that configure the health checks middleware to listen on a preconfigured port. This is sugar for MapWhen, but it's important because a significant set of users will want to use Health Checks in this way. --- .../ManagementPortStartup.cs | 47 +++++ samples/HealthChecksSample/Program.cs | 1 + .../Properties/launchSettings.json | 15 ++ ...HealthCheckApplicationBuilderExtensions.cs | 174 +++++++++++++++++- .../HealthCheckMiddlewareTests.cs | 60 ++++++ 5 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 samples/HealthChecksSample/ManagementPortStartup.cs create mode 100644 samples/HealthChecksSample/Properties/launchSettings.json diff --git a/samples/HealthChecksSample/ManagementPortStartup.cs b/samples/HealthChecksSample/ManagementPortStartup.cs new file mode 100644 index 0000000000..a6ccce97ab --- /dev/null +++ b/samples/HealthChecksSample/ManagementPortStartup.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HealthChecksSample +{ + // Pass in `--scenario port` at the command line to run this sample. + public class ManagementPortStartup + { + public ManagementPortStartup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services.AddHealthChecks(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health but only on the specified port. + // + // By default health checks will return a 200 with 'Healthy'. + // - No health checks are registered by default, the app is healthy if it is reachable + // - The default response writer writes the HealthCheckStatus as text/plain content + // + // Use UseHealthChecks with a port will only process health checks requests on connection + // to the specified port. This is typically used in a container environment where you can expose + // a port for monitoring services to have access to the service. + // - In this case the management is configured in the launchSettings.json and passed through + // and environment variable + // - Additionally, the server is also configured to listen to requests on the management port. + app.UseHealthChecks("/health", port: Configuration["ManagementPort"]); + + app.Run(async (context) => + { + await context.Response.WriteAsync($"Go to http://localhost:{Configuration["ManagementPort"]}/health to see the health status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index b4e8874f22..a5c804e8f0 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -18,6 +18,7 @@ namespace HealthChecksSample { "basic", typeof(BasicStartup) }, { "writer", typeof(CustomWriterStartup) }, { "liveness", typeof(LivenessProbeStartup) }, + { "port", typeof(ManagementPortStartup) }, }; } diff --git a/samples/HealthChecksSample/Properties/launchSettings.json b/samples/HealthChecksSample/Properties/launchSettings.json new file mode 100644 index 0000000000..6afb19b2b3 --- /dev/null +++ b/samples/HealthChecksSample/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "HealthChecksSample": { + "commandName": "Project", + "commandLineArgs": "", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5000/;http://localhost:5001/", + "ASPNETCORE_MANAGEMENTPORT": "5001" + }, + "applicationUrl": "http://localhost:5000/" + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs index e6239a3488..6c41fd5b79 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs @@ -40,7 +40,8 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentException("A URL path must be provided", nameof(path)); } - return app.Map(path, b => b.UseMiddleware()); + UseHealthChecksCore(app, path, port: null, Array.Empty()); + return app; } /// @@ -73,7 +74,176 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(options)); } - return app.Map(path, b => b.UseMiddleware(Options.Create(options))); + UseHealthChecksCore(app, path, port: null, new[] { Options.Create(options), }); + return app; + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// The port to listen on. Must be a local port on which the server is listening. + /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path and port. + /// + /// + /// The health check middleware will use default settings from . + /// + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, int port) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + UseHealthChecksCore(app, path, port, Array.Empty()); + return app; + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// The port to listen on. Must be a local port on which the server is listening. + /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path and port. + /// + /// + /// The health check middleware will use default settings from . + /// + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, string port) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + if (port == null) + { + throw new ArgumentNullException(nameof(port)); + } + + if (!int.TryParse(port, out var portAsInt)) + { + throw new ArgumentException("The port must be a valid integer.", nameof(port)); + } + + UseHealthChecksCore(app, path, portAsInt, Array.Empty()); + return app; + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// The port to listen on. Must be a local port on which the server is listening. + /// A used to configure the middleware. + /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path. + /// + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, int port, HealthCheckOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + UseHealthChecksCore(app, path, port, new[] { Options.Create(options), }); + return app; + } + + /// + /// Adds a middleware that provides health check status. + /// + /// The . + /// The path on which to provide health check status. + /// The port to listen on. Must be a local port on which the server is listening. + /// A used to configure the middleware. + /// A reference to the after the operation has completed. + /// + /// + /// This method will use to + /// listen to health checks requests on the specified URL path. + /// + /// + public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, string port, HealthCheckOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!path.HasValue) + { + throw new ArgumentException("A URL path must be provided", nameof(path)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(port)); + } + + if (!int.TryParse(port, out var portAsInt)) + { + throw new ArgumentException("The port must be a valid integer.", nameof(port)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + UseHealthChecksCore(app, path, portAsInt, new[] { Options.Create(options), }); + return app; + } + + private static void UseHealthChecksCore(IApplicationBuilder app, PathString path, int? port, object[] args) + { + if (port == null) + { + app.Map(path, b => b.UseMiddleware(args)); + } + else + { + app.MapWhen( + c => c.Connection.LocalPort == port && c.Request.Path.StartsWithSegments(path), + b => b.UseMiddleware(args)); + } } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 0555ab6f7a..692e8b5bfd 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -355,5 +355,65 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks "The following health checks were not found: 'Bazzzzzz'. Registered health checks: 'Foo, Bar, Baz'.", ex.Message); } + + [Fact] + public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(next => async (context) => + { + // Need to fake setting the connection info. TestServer doesn't + // do that, because it doesn't have a connection. + context.Connection.LocalPort = context.Request.Host.Port.Value; + await next(context); + }); + + app.UseHealthChecks("/health", port: 5001); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5001/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CanListenOnPort_RejectsRequest_OnOtherPort() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(next => async (context) => + { + // Need to fake setting the connection info. TestServer doesn't + // do that, because it doesn't have a connection. + context.Connection.LocalPort = context.Request.Host.Port.Value; + await next(context); + }); + + app.UseHealthChecks("/health", port: 5001); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5000/health"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } } } From d1cba1f55bab1e3b206a46cee81eb1583d8732e2 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 3 Aug 2018 14:47:20 -0700 Subject: [PATCH 26/57] Add Database health sample --- build/dependencies.props | 2 + samples/HealthChecksSample/DBHealthStartup.cs | 47 ++++++++++++++ .../DbConnectionHealthCheck.cs | 64 +++++++++++++++++++ .../HealthChecksSample.csproj | 2 + samples/HealthChecksSample/Program.cs | 4 ++ .../SqlConnectionHealthCheck.cs | 26 ++++++++ samples/HealthChecksSample/appsettings.json | 5 ++ 7 files changed, 150 insertions(+) create mode 100644 samples/HealthChecksSample/DBHealthStartup.cs create mode 100644 samples/HealthChecksSample/DbConnectionHealthCheck.cs create mode 100644 samples/HealthChecksSample/SqlConnectionHealthCheck.cs create mode 100644 samples/HealthChecksSample/appsettings.json diff --git a/build/dependencies.props b/build/dependencies.props index c201e62915..c9c79ef2dc 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -17,6 +17,7 @@ 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 + 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 2.2.0-preview1-34823 @@ -37,6 +38,7 @@ 4.7.49 2.0.3 11.0.2 + 4.6.0-preview1-26725-04 4.5.0 1.6.0 2.3.1 diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs new file mode 100644 index 0000000000..f82f4c54ef --- /dev/null +++ b/samples/HealthChecksSample/DBHealthStartup.cs @@ -0,0 +1,47 @@ + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HealthChecksSample +{ + // Pass in `--scenario db` at the command line to run this sample. + public class DBHealthStartup + { + public DBHealthStartup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services.AddHealthChecks() + + // Add a health check for a SQL database + .AddCheck(new SqlConnectionHealthCheck("MyDatabase", Configuration["ConnectionStrings:DefaultConnection"])); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health. + // + // By default health checks will return a 200 with 'Healthy' when the database is responsive + // - We've registered a SqlConnectionHealthCheck + // - The default response writer writes the HealthCheckStatus as text/plain content + // + // This is the simplest way to use health checks, it is suitable for systems + // that want to check for 'liveness' of an application with a database. + app.UseHealthChecks("/health"); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status"); + }); + } + } +} diff --git a/samples/HealthChecksSample/DbConnectionHealthCheck.cs b/samples/HealthChecksSample/DbConnectionHealthCheck.cs new file mode 100644 index 0000000000..00d9bd4803 --- /dev/null +++ b/samples/HealthChecksSample/DbConnectionHealthCheck.cs @@ -0,0 +1,64 @@ +// 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. + +using System; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + public abstract class DbConnectionHealthCheck : IHealthCheck + { + protected DbConnectionHealthCheck(string name, string connectionString) + : this(name, connectionString, testQuery: null) + { + } + + protected DbConnectionHealthCheck(string name, string connectionString, string testQuery) + { + Name = name ?? throw new System.ArgumentNullException(nameof(name)); + ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); + TestQuery = testQuery; + } + + public string Name { get; } + + protected string ConnectionString { get; } + + // This sample supports specifying a query to run as a boolean test of whether the database + // is responding. It is important to choose a query that will return quickly or you risk + // overloading the database. + // + // In most cases this is not necessary, but if you find it necessary, choose a simple query such as 'SELECT 1'. + protected string TestQuery { get; } + + protected abstract DbConnection CreateConnection(string connectionString); + + public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + using (var connection = CreateConnection(ConnectionString)) + { + try + { + await connection.OpenAsync(cancellationToken); + + if (TestQuery != null) + { + var command = connection.CreateCommand(); + command.CommandText = TestQuery; + + await command.ExecuteNonQueryAsync(cancellationToken); + } + } + catch (DbException ex) + { + return HealthCheckResult.Unhealthy(ex); + } + } + + return HealthCheckResult.Healthy(); + } + } +} diff --git a/samples/HealthChecksSample/HealthChecksSample.csproj b/samples/HealthChecksSample/HealthChecksSample.csproj index c8a2fded73..b013e687fa 100644 --- a/samples/HealthChecksSample/HealthChecksSample.csproj +++ b/samples/HealthChecksSample/HealthChecksSample.csproj @@ -8,10 +8,12 @@ + + diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index a5c804e8f0..d24e0b38c1 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -19,6 +20,7 @@ namespace HealthChecksSample { "writer", typeof(CustomWriterStartup) }, { "liveness", typeof(LivenessProbeStartup) }, { "port", typeof(ManagementPortStartup) }, + { "db", typeof(DBHealthStartup) }, }; } @@ -30,6 +32,8 @@ namespace HealthChecksSample public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") .AddEnvironmentVariables(prefix: "ASPNETCORE_") .AddCommandLine(args) .Build(); diff --git a/samples/HealthChecksSample/SqlConnectionHealthCheck.cs b/samples/HealthChecksSample/SqlConnectionHealthCheck.cs new file mode 100644 index 0000000000..5849da2202 --- /dev/null +++ b/samples/HealthChecksSample/SqlConnectionHealthCheck.cs @@ -0,0 +1,26 @@ +using System.Data.Common; +using System.Data.SqlClient; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace HealthChecksSample +{ + public class SqlConnectionHealthCheck : DbConnectionHealthCheck + { + private static readonly string DefaultTestQuery = "Select 1"; + + public SqlConnectionHealthCheck(string name, string connectionString) + : this(name, connectionString, testQuery: DefaultTestQuery) + { + } + + public SqlConnectionHealthCheck(string name, string connectionString, string testQuery) + : base(name, connectionString, testQuery ?? DefaultTestQuery) + { + } + + protected override DbConnection CreateConnection(string connectionString) + { + return new SqlConnection(connectionString); + } + } +} diff --git a/samples/HealthChecksSample/appsettings.json b/samples/HealthChecksSample/appsettings.json new file mode 100644 index 0000000000..77b5f890a4 --- /dev/null +++ b/samples/HealthChecksSample/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;ConnectRetryCount=0" + } +} \ No newline at end of file From 701fe391706931e8a155a4176d3b45471529cd90 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 5 Aug 2018 19:10:57 +0000 Subject: [PATCH 27/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index c9c79ef2dc..e7b4374965 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,34 +3,34 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17102 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 - 2.2.0-preview1-34823 + 2.2.0-preview1-20180731.1 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 + 2.2.0-preview1-34882 2.0.9 2.1.2 2.2.0-preview1-26618-02 @@ -38,7 +38,7 @@ 4.7.49 2.0.3 11.0.2 - 4.6.0-preview1-26725-04 + 4.5.1 4.5.0 1.6.0 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 6b8da29e6b..c7af2292c7 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17102 -commithash:e7e2b5a97ca92cfc6acc4def534cb0901a6d1eb9 +version:2.2.0-preview1-20180731.1 +commithash:29fde58465439f4bb9df40830635ed758e063daf From 602faf5baa7cad3bb249e4db3fcc8530be52a200 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Mon, 6 Aug 2018 20:33:14 +0000 Subject: [PATCH 28/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index e7b4374965..cfb2fe738f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,33 +4,33 @@ 2.2.0-preview1-20180731.1 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 - 2.2.0-preview1-34882 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 + 2.2.0-preview1-34896 2.0.9 2.1.2 2.2.0-preview1-26618-02 From 8ec69456ca7afa05e794934361c3417227e2169f Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 6 Aug 2018 16:44:54 -0700 Subject: [PATCH 29/57] Update CustomWriterStartup.cs Fix a misleading comment in sample --- samples/HealthChecksSample/CustomWriterStartup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/HealthChecksSample/CustomWriterStartup.cs b/samples/HealthChecksSample/CustomWriterStartup.cs index 97003068fc..0c3e542b72 100644 --- a/samples/HealthChecksSample/CustomWriterStartup.cs +++ b/samples/HealthChecksSample/CustomWriterStartup.cs @@ -35,7 +35,7 @@ namespace HealthChecksSample // check result in a totally custom way. app.UseHealthChecks("/health", new HealthCheckOptions() { - // This custom writer formats the detailed status as an HTML table. + // This custom writer formats the detailed status as JSON. ResponseWriter = WriteResponse, }); From 983d7836ab27bce47a86e5425860e8ac1ff2b858 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 12 Aug 2018 19:10:01 +0000 Subject: [PATCH 30/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 56 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index cfb2fe738f..b83b28598c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,34 +3,34 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180731.1 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 - 2.2.0-preview1-34896 + 2.2.0-preview1-20180807.2 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 + 2.2.0-preview1-34967 2.0.9 2.1.2 2.2.0-preview1-26618-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index c7af2292c7..3fbcc80189 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180731.1 -commithash:29fde58465439f4bb9df40830635ed758e063daf +version:2.2.0-preview1-20180807.2 +commithash:11495dbd236104434e08cb1152fcb58cf2a20923 From 5c8b5498820ca4cd1a3ac205eae5643b1a1463ac Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Tue, 21 Aug 2018 13:33:49 -0700 Subject: [PATCH 31/57] Update package branding for 2.2.0-preview2 --- build/dependencies.props | 2 +- version.props | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index b83b28598c..b8974fdba7 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -44,6 +44,6 @@ 2.3.1 2.4.0 - + diff --git a/version.props b/version.props index 8e872d018e..f77be62107 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ 2.2.0 - preview1 + preview2 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 @@ -10,7 +10,7 @@ $(VersionSuffix)-$(BuildNumber) 0.5.0 - preview1 + preview2 $(ExperimentalVersionPrefix) $(ExperimentalVersionPrefix)-$(ExperimentalVersionSuffix)-final $(ExperimentalVersionSuffix)-$(BuildNumber) From fc7fa4f0f073f61f08b16df602b5e83f61c7b557 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sun, 26 Aug 2018 11:12:54 +0100 Subject: [PATCH 32/57] Fix sample typo Fix typo in sample comment. --- samples/HealthChecksSample/LivenessProbeStartup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs index 3a4078759f..e927412456 100644 --- a/samples/HealthChecksSample/LivenessProbeStartup.cs +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -50,7 +50,7 @@ namespace HealthChecksSample // The readiness check uses all of the registered health checks (default) app.UseHealthChecks("/health/ready"); - // The liveness check uses an 'identity' health check that always returns healty + // The liveness check uses an 'identity' health check that always returns healthy app.UseHealthChecks("/health/live", new HealthCheckOptions() { // Filters the set of health checks run by this middleware From 6cd4cf1a7a19b35c87f9c123ecf390b700d4bec8 Mon Sep 17 00:00:00 2001 From: Zhiliang Date: Mon, 27 Aug 2018 17:25:47 +0800 Subject: [PATCH 33/57] Fix typos in samples --- samples/HealthChecksSample/LivenessProbeStartup.cs | 2 +- samples/HealthChecksSample/ManagementPortStartup.cs | 2 +- samples/HealthChecksSample/SlowDependencyHealthCheck.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs index e927412456..f5eabc6696 100644 --- a/samples/HealthChecksSample/LivenessProbeStartup.cs +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -43,7 +43,7 @@ namespace HealthChecksSample // // In this example, the liveness check will us an 'identity' check that always returns healthy. // - // In this example, the readiness check will run all registered checks, include a check with an + // In this example, the readiness check will run all registered checks, include a check with a // long initialization time (15 seconds). diff --git a/samples/HealthChecksSample/ManagementPortStartup.cs b/samples/HealthChecksSample/ManagementPortStartup.cs index a6ccce97ab..79b38dc875 100644 --- a/samples/HealthChecksSample/ManagementPortStartup.cs +++ b/samples/HealthChecksSample/ManagementPortStartup.cs @@ -34,7 +34,7 @@ namespace HealthChecksSample // to the specified port. This is typically used in a container environment where you can expose // a port for monitoring services to have access to the service. // - In this case the management is configured in the launchSettings.json and passed through - // and environment variable + // an environment variable // - Additionally, the server is also configured to listen to requests on the management port. app.UseHealthChecks("/health", port: Configuration["ManagementPort"]); diff --git a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs index 1ca03f88ae..7832724c81 100644 --- a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs +++ b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthChecksSample { - // Simulates a health check for an application dependency that takes a while to initialize. + // Simulates a health check for an application dependency that takes a while to initialize. // This is part of the readiness/liveness probe sample. public class SlowDependencyHealthCheck : IHealthCheck { From 3e4a3d0b9013f8b41ff848d1de5c3a0d45c359c8 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 30 Aug 2018 10:51:48 -0700 Subject: [PATCH 34/57] Allow health checks to use any DI lifetime (#466) * Allow health checks to use any DI lifetime This change allows registered IHealthCheck implementations to use any DI lifetime. This is necessary for scenarios like using EF which requires a scope. The works by having the health check service create a scope for each time it queries health checks. This scope does not overlap or share state with other scopes (the request scope) so there is no crosstalk between processing going on per-request in ASP.NET Core and the health check operation. * PR feedback and some logging cleanup --- build/dependencies.props | 1 + samples/HealthChecksSample/DBHealthStartup.cs | 1 - .../LivenessProbeStartup.cs | 8 +- samples/HealthChecksSample/Program.cs | 4 +- samples/HealthChecksSample/appsettings.json | 8 + .../HealthCheckMiddleware.cs | 5 +- .../HealthCheckOptions.cs | 12 +- .../HealthCheckService.cs | 206 +++++++++-------- .../HealthChecksBuilderAddCheckExtensions.cs | 24 +- .../IHealthCheckService.cs | 40 ++-- ...Extensions.Diagnostics.HealthChecks.csproj | 1 + .../Properties/AssemblyInfo.cs | 3 + .../HealthCheckMiddlewareTests.cs | 35 +-- .../HealthCheckServiceTests.cs | 212 +++++++++++++----- .../HealthChecksBuilderTests.cs | 11 +- 15 files changed, 346 insertions(+), 225 deletions(-) create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs diff --git a/build/dependencies.props b/build/dependencies.props index b8974fdba7..ad2a6ed050 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -31,6 +31,7 @@ 2.2.0-preview1-34967 2.2.0-preview1-34967 2.2.0-preview1-34967 + 2.2.0-preview1-34967 2.0.9 2.1.2 2.2.0-preview1-26618-02 diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs index f82f4c54ef..58b6c8d157 100644 --- a/samples/HealthChecksSample/DBHealthStartup.cs +++ b/samples/HealthChecksSample/DBHealthStartup.cs @@ -21,7 +21,6 @@ namespace HealthChecksSample { // Registers required services for health checks services.AddHealthChecks() - // Add a health check for a SQL database .AddCheck(new SqlConnectionHealthCheck("MyDatabase", Configuration["ConnectionStrings:DefaultConnection"])); } diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs index f5eabc6696..aa644d231f 100644 --- a/samples/HealthChecksSample/LivenessProbeStartup.cs +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -17,7 +17,6 @@ namespace HealthChecksSample // Registers required services for health checks services .AddHealthChecks() - .AddCheck("identity", () => Task.FromResult(HealthCheckResult.Healthy())) .AddCheck(new SlowDependencyHealthCheck()); } @@ -53,11 +52,8 @@ namespace HealthChecksSample // The liveness check uses an 'identity' health check that always returns healthy app.UseHealthChecks("/health/live", new HealthCheckOptions() { - // Filters the set of health checks run by this middleware - HealthCheckNames = - { - "identity", - }, + // Exclude all checks, just return a 200. + Predicate = (check) => false, }); app.Run(async (context) => diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index d24e0b38c1..425b0b3f1a 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -15,7 +15,7 @@ namespace HealthChecksSample { _scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "", typeof(BasicStartup) }, + { "", typeof(DBHealthStartup) }, { "basic", typeof(BasicStartup) }, { "writer", typeof(CustomWriterStartup) }, { "liveness", typeof(LivenessProbeStartup) }, @@ -48,6 +48,8 @@ namespace HealthChecksSample .UseConfiguration(config) .ConfigureLogging(builder => { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddConfiguration(config); builder.AddConsole(); }) .UseKestrel() diff --git a/samples/HealthChecksSample/appsettings.json b/samples/HealthChecksSample/appsettings.json index 77b5f890a4..21b2dcdbfd 100644 --- a/samples/HealthChecksSample/appsettings.json +++ b/samples/HealthChecksSample/appsettings.json @@ -1,5 +1,13 @@ { "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;ConnectRetryCount=0" + }, + "Logging": { + "LogLevel": { + "Default": "Debug" + }, + "Console": { + "IncludeScopes": "true" + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 3a220f2595..20ec5e356e 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -16,7 +16,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks private readonly RequestDelegate _next; private readonly HealthCheckOptions _healthCheckOptions; private readonly IHealthCheckService _healthCheckService; - private readonly IHealthCheck[] _checks; public HealthCheckMiddleware( RequestDelegate next, @@ -41,8 +40,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks _next = next; _healthCheckOptions = healthCheckOptions.Value; _healthCheckService = healthCheckService; - - _checks = FilterHealthChecks(_healthCheckService.Checks, healthCheckOptions.Value.HealthCheckNames); } /// @@ -58,7 +55,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks } // Get results - var result = await _healthCheckService.CheckHealthAsync(_checks, httpContext.RequestAborted); + var result = await _healthCheckService.CheckHealthAsync(_healthCheckOptions.Predicate, httpContext.RequestAborted); // Map status to response code - this is customizable via options. if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index b57374f2ae..7930d25577 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -15,15 +15,19 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks public class HealthCheckOptions { /// - /// Gets a set of health check names used to filter the set of health checks run. + /// Gets or sets a predicate that is used to filter the set of health checks executed. /// /// - /// If is empty, the will run all + /// If is null, the will run all /// registered health checks - this is the default behavior. To run a subset of health checks, - /// add the names of the desired health checks. + /// provide a function that filters the set of checks. /// - public ISet HealthCheckNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); + public Func Predicate { get; set; } + /// + /// Gets a dictionary mapping the to an HTTP status code applied to the response. + /// This property can be used to configure the status codes returned for each status. + /// public IDictionary ResultStatusCodes { get; } = new Dictionary() { { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs index 35cc0a1c82..111113ea77 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs @@ -6,127 +6,139 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.Diagnostics.HealthChecks { - /// - /// Default implementation of . - /// - public class HealthCheckService : IHealthCheckService + internal class HealthCheckService : IHealthCheckService { + private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; - /// - /// A containing all the health checks registered in the application. - /// - /// - /// The key maps to the property of the health check, and the value is the - /// instance itself. - /// - public IReadOnlyDictionary Checks { get; } - - /// - /// Constructs a from the provided collection of instances. - /// - /// The instances that have been registered in the application. - public HealthCheckService(IEnumerable healthChecks) : this(healthChecks, NullLogger.Instance) { } - - /// - /// Constructs a from the provided collection of instances, and the provided logger. - /// - /// The instances that have been registered in the application. - /// A that can be used to log events that occur during health check operations. - public HealthCheckService(IEnumerable healthChecks, ILogger logger) + public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger logger) { - healthChecks = healthChecks ?? throw new ArgumentNullException(nameof(healthChecks)); + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - // Scan the list for duplicate names to provide a better error if there are duplicates. - var names = new HashSet(StringComparer.OrdinalIgnoreCase); - var duplicates = new List(); - foreach (var check in healthChecks) + // 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 + // actually tries to **run** health checks would be real baaaaad. + using (var scope = _scopeFactory.CreateScope()) { - if (!names.Add(check.Name)) - { - duplicates.Add(check.Name); - } - } - - if (duplicates.Count > 0) - { - throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicates)}", nameof(healthChecks)); - } - - Checks = healthChecks.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase); - - if (_logger.IsEnabled(LogLevel.Debug)) - { - foreach (var check in Checks) - { - _logger.LogDebug("Health check '{healthCheckName}' has been registered", check.Key); - } + var healthChecks = scope.ServiceProvider.GetRequiredService>(); + EnsureNoDuplicates(healthChecks); } } - /// - /// Runs all the health checks in the application and returns the aggregated status. - /// - /// A which can be used to cancel the health checks. - /// - /// A which will complete when all the health checks have been run, - /// yielding a containing the results. - /// public Task CheckHealthAsync(CancellationToken cancellationToken = default) => - CheckHealthAsync(Checks.Values, cancellationToken); - - /// - /// Runs the provided health checks and returns the aggregated status - /// - /// The instances to be run. - /// A which can be used to cancel the health checks. - /// - /// A which will complete when all the health checks have been run, - /// yielding a containing the results. - /// - public async Task CheckHealthAsync(IEnumerable checks, CancellationToken cancellationToken = default) + CheckHealthAsync(predicate: null, cancellationToken); + + public async Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default) { - var results = new Dictionary(Checks.Count, StringComparer.OrdinalIgnoreCase); - foreach (var check in checks) + using (var scope = _scopeFactory.CreateScope()) { - cancellationToken.ThrowIfCancellationRequested(); - // If the health check does things like make Database queries using EF or backend HTTP calls, - // it may be valuable to know that logs it generates are part of a health check. So we start a scope. - using (_logger.BeginScope(new HealthCheckLogScope(check.Name))) + var healthChecks = scope.ServiceProvider.GetRequiredService>(); + + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var healthCheck in healthChecks) { - HealthCheckResult result; - try + if (predicate != null && !predicate(healthCheck)) { - _logger.LogTrace("Running health check: {healthCheckName}", check.Name); - result = await check.CheckHealthAsync(cancellationToken); - _logger.LogTrace("Health check '{healthCheckName}' completed with status '{healthCheckStatus}'", check.Name, result.Status); - } - catch (Exception ex) - { - // We don't log this as an error because a health check failing shouldn't bring down the active task. - _logger.LogError(ex, "Health check '{healthCheckName}' threw an unexpected exception", check.Name); - result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null); + continue; } - // This can only happen if the result is default(HealthCheckResult) - if (result.Status == HealthCheckStatus.Unknown) - { - // This is different from the case above. We throw here because a health check is doing something specifically incorrect. - var exception = new InvalidOperationException($"Health check '{check.Name}' returned a result with a status of Unknown"); - _logger.LogError(exception, "Health check '{healthCheckName}' returned a result with a status of Unknown", check.Name); - throw exception; - } + cancellationToken.ThrowIfCancellationRequested(); - results[check.Name] = result; + // If the health check does things like make Database queries using EF or backend HTTP calls, + // it may be valuable to know that logs it generates are part of a health check. So we start a scope. + using (_logger.BeginScope(new HealthCheckLogScope(healthCheck.Name))) + { + HealthCheckResult result; + try + { + Log.HealthCheckBegin(_logger, healthCheck); + var stopwatch = ValueStopwatch.StartNew(); + result = await healthCheck.CheckHealthAsync(cancellationToken); + Log.HealthCheckEnd(_logger, healthCheck, result, stopwatch.GetElapsedTime()); + } + catch (Exception ex) + { + Log.HealthCheckError(_logger, healthCheck, ex); + result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null); + } + + // This can only happen if the result is default(HealthCheckResult) + if (result.Status == HealthCheckStatus.Unknown) + { + // This is different from the case above. We throw here because a health check is doing something specifically incorrect. + throw new InvalidOperationException($"Health check '{healthCheck.Name}' returned a result with a status of Unknown"); + } + + results[healthCheck.Name] = result; + } } + + return new CompositeHealthCheckResult(results); + } + } + + private static void EnsureNoDuplicates(IEnumerable healthChecks) + { + // Scan the list for duplicate names to provide a better error if there are duplicates. + var duplicateNames = healthChecks + .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + if (duplicateNames.Count > 0) + { + throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(healthChecks)); + } + } + + private static class Log + { + public static class EventIds + { + public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); + public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); + public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError"); + } + + private static readonly Action _healthCheckBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckBegin, + "Running health check {HealthCheckName}"); + + private static readonly Action _healthCheckEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckEnd, + "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}"); + + private static readonly Action _healthCheckError = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckError, + "Health check {HealthCheckName} threw an unhandled exception"); + + public static void HealthCheckBegin(ILogger logger, IHealthCheck healthCheck) + { + _healthCheckBegin(logger, healthCheck.Name, null); + } + + public static void HealthCheckEnd(ILogger logger, IHealthCheck healthCheck, HealthCheckResult result, TimeSpan duration) + { + _healthCheckEnd(logger, healthCheck.Name, duration.TotalMilliseconds, result.Status, null); + } + + public static void HealthCheckError(ILogger logger, IHealthCheck healthCheck, Exception exception) + { + _healthCheckError(logger, healthCheck.Name, exception); } - return new CompositeHealthCheckResult(results); } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs index b2f931fcd3..d3af5b17c2 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs @@ -68,7 +68,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Adds a new health check with the implementation. + /// Adds a new health check with the provided implementation. /// /// The to add the check to. /// An implementation. @@ -88,5 +88,27 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.AddSingleton(check); return builder; } + + /// + /// Adds a new health check as a transient dependency injected service with the provided type. + /// + /// The health check implementation type. + /// The . + /// The . + /// + /// This method will register a transient service of type with the + /// provided implementation type . Using this method to register a health + /// check allows you to register a health check that depends on transient and scoped services. + /// + public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.Add(ServiceDescriptor.Transient(typeof(IHealthCheck), typeof(T))); + return builder; + } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs index 9c1bfbafc4..f931475a4d 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs @@ -1,26 +1,38 @@ // 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. -using System.Collections.Generic; +using System; using System.Threading; using System.Threading.Tasks; namespace Microsoft.Extensions.Diagnostics.HealthChecks { /// - /// A service which can be used to check the status of instances registered in the application. + /// A service which can be used to check the status of instances + /// registered in the application. /// + /// + /// + /// The default implementation of is registered in the dependency + /// injection container as a singleton service by calling + /// . + /// + /// + /// The returned by + /// + /// provides a convenience API for registering health checks. + /// + /// + /// The default implementation of will use all services + /// of type registered in the dependency injection container. + /// implementations may be registered with any service lifetime. The implementation will create a scope + /// for each aggregate health check operation and use the scope to resolve services. The scope + /// created for executing health checks is controlled by the health checks service and does not + /// share scoped services with any other scope in the application. + /// + /// public interface IHealthCheckService { - /// - /// A containing all the health checks registered in the application. - /// - /// - /// The key maps to the property of the health check, and the value is the - /// instance itself. - /// - IReadOnlyDictionary Checks { get; } - /// /// Runs all the health checks in the application and returns the aggregated status. /// @@ -34,13 +46,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// /// Runs the provided health checks and returns the aggregated status /// - /// The instances to be run. + /// + /// A predicate that can be used to include health checks based on user-defined criteria. + /// /// A which can be used to cancel the health checks. /// /// A which will complete when all the health checks have been run, /// yielding a containing the results. /// - Task CheckHealthAsync(IEnumerable checks, + Task CheckHealthAsync(Func predicate, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index 56f70f7a10..2b3d0e6261 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -14,6 +14,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder + diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..13e969bfad --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 692e8b5bfd..1af49d8422 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -302,11 +302,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { app.UseHealthChecks("/health", new HealthCheckOptions() { - HealthCheckNames = - { - "Baz", - "FOO", - }, + Predicate = (check) => check.Name == "Foo" || check.Name == "Baz", }); }) .ConfigureServices(services => @@ -327,35 +323,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } - [Fact] - public void CanFilterChecks_ThrowsForMissingCheck() - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseHealthChecks("/health", new HealthCheckOptions() - { - HealthCheckNames = - { - "Bazzzzzz", - "FOO", - }, - }); - }) - .ConfigureServices(services => - { - services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); - }); - - var ex = Assert.Throws(() => new TestServer(builder)); - Assert.Equal( - "The following health checks were not found: 'Bazzzzzz'. Registered health checks: 'Foo, Bar, Baz'.", - ex.Message); - } - [Fact] public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort() { diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs index 2a7fa8395f..6e7c28e9ef 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Xunit; @@ -12,40 +14,30 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { public class HealthCheckServiceTests { - [Fact] - public void Constructor_BuildsDictionaryOfChecks() - { - // Arrange - var fooCheck = new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())); - var barCheck = new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())); - var bazCheck = new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())); - var checks = new[] { fooCheck, barCheck, bazCheck }; - - // Act - var service = new HealthCheckService(checks); - - // Assert - Assert.Same(fooCheck, service.Checks["Foo"]); - Assert.Same(barCheck, service.Checks["Bar"]); - Assert.Same(bazCheck, service.Checks["Baz"]); - Assert.Equal(3, service.Checks.Count); - } - [Fact] public void Constructor_ThrowsUsefulExceptionForDuplicateNames() { // Arrange - var checks = new[] - { - new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())), - new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())), - new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())), - new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())), - new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())), - }; + // + // Doing this the old fashioned way so we can verify that the exception comes + // from the constructor. + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddOptions(); + serviceCollection.AddHealthChecks() + .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) + .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) + .AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy()))) + .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))) + .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))); + + var services = serviceCollection.BuildServiceProvider(); + + var scopeFactory = services.GetRequiredService(); + var logger = services.GetRequiredService>(); // Act - var exception = Assert.Throws(() => new HealthCheckService(checks)); + var exception = Assert.Throws(() => new HealthCheckService(scopeFactory, logger)); // Assert Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message); @@ -67,15 +59,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { DataKey, DataValue } }; - var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); - var degradedCheck = new HealthCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); - var unhealthyCheck = new HealthCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); - - var service = new HealthCheckService(new[] + var service = CreateHealthChecksService(b => { - healthyCheck, - degradedCheck, - unhealthyCheck, + b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); + b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); + b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); }); // Act @@ -85,7 +73,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks Assert.Collection(results.Results, actual => { - Assert.Equal(healthyCheck.Name, actual.Key); + Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); @@ -97,7 +85,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks }, actual => { - Assert.Equal(degradedCheck.Name, actual.Key); + Assert.Equal("DegradedCheck", actual.Key); Assert.Equal(DegradedMessage, actual.Value.Description); Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status); Assert.Null(actual.Value.Exception); @@ -105,7 +93,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks }, actual => { - Assert.Equal(unhealthyCheck.Name, actual.Key); + Assert.Equal("UnhealthyCheck", actual.Key); Assert.Equal(UnhealthyMessage, actual.Value.Description); Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status); Assert.Same(exception, actual.Value.Exception); @@ -114,7 +102,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks } [Fact] - public async Task CheckAsync_RunsProvidedChecksAndAggregatesResultsAsync() + public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync() { const string DataKey = "Foo"; const string DataValue = "Bar"; @@ -129,28 +117,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { DataKey, DataValue } }; - var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); - var degradedCheck = new HealthCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); - var unhealthyCheck = new HealthCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); - - var service = new HealthCheckService(new[] + var service = CreateHealthChecksService(b => { - healthyCheck, - degradedCheck, - unhealthyCheck, + b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); + b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); + b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); }); // Act - var results = await service.CheckHealthAsync(new[] - { - service.Checks["HealthyCheck"] - }); + var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck"); // Assert Assert.Collection(results.Results, actual => { - Assert.Equal(healthyCheck.Name, actual.Key); + Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); @@ -168,11 +149,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Arrange var thrownException = new InvalidOperationException("Whoops!"); var faultedException = new InvalidOperationException("Ohnoes!"); - var service = new HealthCheckService(new[] + + var service = CreateHealthChecksService(b => { - new HealthCheck("Throws", ct => throw thrownException), - new HealthCheck("Faults", ct => Task.FromException(faultedException)), - new HealthCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())), + b.AddCheck("Throws", ct => throw thrownException); + b.AddCheck("Faults", ct => Task.FromException(faultedException)); + b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())); }); // Act @@ -223,8 +205,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks }); return Task.FromResult(HealthCheckResult.Healthy()); }); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); - var service = new HealthCheckService(new[] { check }, loggerFactory.CreateLogger()); + var service = CreateHealthChecksService(b => + { + // Override the logger factory for testing + b.Services.AddSingleton(loggerFactory); + + b.AddCheck(check); + }); // Act var results = await service.CheckHealthAsync(); @@ -241,9 +230,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult() { // Arrange - var service = new HealthCheckService(new[] + var service = CreateHealthChecksService(b => { - new HealthCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))), + b.AddCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))); }); // Act @@ -252,5 +241,108 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message); } + + [Fact] + public async Task CheckHealthAsync_CheckCanDependOnTransientService() + { + // Arrange + var service = CreateHealthChecksService(b => + { + b.Services.AddTransient(); + + b.AddCheck(); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Results, + actual => + { + Assert.Equal("Test", actual.Key); + Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + }); + } + + [Fact] + public async Task CheckHealthAsync_CheckCanDependOnScopedService() + { + // Arrange + var service = CreateHealthChecksService(b => + { + b.Services.AddScoped(); + + b.AddCheck(); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Results, + actual => + { + Assert.Equal("Test", actual.Key); + Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + }); + } + + [Fact] + public async Task CheckHealthAsync_CheckCanDependOnSingletonService() + { + // Arrange + var service = CreateHealthChecksService(b => + { + b.Services.AddSingleton(); + + b.AddCheck(); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Results, + actual => + { + Assert.Equal("Test", actual.Key); + Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + }); + } + + private static HealthCheckService CreateHealthChecksService(Action configure) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + + var builder = services.AddHealthChecks(); + if (configure != null) + { + configure(builder); + } + + return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); + } + + private class AnotherService { } + + private class CheckWithServiceDependency : IHealthCheck + { + public CheckWithServiceDependency(AnotherService _) + { + } + + public string Name => "Test"; + + public Task CheckHealthAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(HealthCheckResult.Healthy()); + } + } } } diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs index a24a4a5b4e..f1bcbbf00b 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs @@ -1,6 +1,8 @@ // 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. +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -19,12 +21,13 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy())); // Act - var healthCheckService = services.BuildServiceProvider().GetRequiredService(); + var checks = services.BuildServiceProvider().GetRequiredService>(); // Assert - Assert.Collection(healthCheckService.Checks, - actual => Assert.Equal("Foo", actual.Key), - actual => Assert.Equal("Bar", actual.Key)); + Assert.Collection( + checks, + actual => Assert.Equal("Foo", actual.Name), + actual => Assert.Equal("Bar", actual.Name)); } } } From 525fbf495b6826a386e7ff29561f86213e0ecfbc Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 30 Aug 2018 15:29:54 -0700 Subject: [PATCH 35/57] Fix #468 - all UseHealthChecks without a path --- ...HealthCheckApplicationBuilderExtensions.cs | 34 +---------- .../HealthCheckMiddlewareTests.cs | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs index 6c41fd5b79..6463889f43 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs @@ -35,11 +35,6 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(app)); } - if (!path.HasValue) - { - throw new ArgumentException("A URL path must be provided", nameof(path)); - } - UseHealthChecksCore(app, path, port: null, Array.Empty()); return app; } @@ -64,11 +59,6 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(app)); } - if (!path.HasValue) - { - throw new ArgumentException("A URL path must be provided", nameof(path)); - } - if (options == null) { throw new ArgumentNullException(nameof(options)); @@ -101,11 +91,6 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(app)); } - if (!path.HasValue) - { - throw new ArgumentException("A URL path must be provided", nameof(path)); - } - UseHealthChecksCore(app, path, port, Array.Empty()); return app; } @@ -133,11 +118,6 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(app)); } - if (!path.HasValue) - { - throw new ArgumentException("A URL path must be provided", nameof(path)); - } - if (port == null) { throw new ArgumentNullException(nameof(port)); @@ -173,11 +153,6 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(app)); } - if (!path.HasValue) - { - throw new ArgumentException("A URL path must be provided", nameof(path)); - } - if (options == null) { throw new ArgumentNullException(nameof(options)); @@ -208,11 +183,6 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(app)); } - if (!path.HasValue) - { - throw new ArgumentException("A URL path must be provided", nameof(path)); - } - if (path == null) { throw new ArgumentNullException(nameof(port)); @@ -241,8 +211,8 @@ namespace Microsoft.AspNetCore.Builder else { app.MapWhen( - c => c.Connection.LocalPort == port && c.Request.Path.StartsWithSegments(path), - b => b.UseMiddleware(args)); + c => c.Connection.LocalPort == port, + b0 => b0.Map(path, b1 => b1.UseMiddleware(args))); } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 1af49d8422..16fe68ac2a 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -323,6 +323,29 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } + [Fact] + public async Task CanListenWithoutPath_AcceptsRequest() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks(default); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5001/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + [Fact] public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort() { @@ -354,6 +377,37 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } + [Fact] + public async Task CanListenOnPortWithoutPath_AcceptsRequest_OnSpecifiedPort() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(next => async (context) => + { + // Need to fake setting the connection info. TestServer doesn't + // do that, because it doesn't have a connection. + context.Connection.LocalPort = context.Request.Host.Port.Value; + await next(context); + }); + + app.UseHealthChecks(default, port: 5001); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5001/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + [Fact] public async Task CanListenOnPort_RejectsRequest_OnOtherPort() { @@ -382,5 +436,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + + } } From dd07e6743c9a9cdd45d48017a4a5ee932ea3188e Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 2 Sep 2018 12:09:26 -0700 Subject: [PATCH 36/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index ad2a6ed050..cd53ae3566 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,35 +3,35 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180807.2 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 - 2.2.0-preview1-34967 + 2.2.0-preview1-20180821.1 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 + 2.2.0-preview2-35143 2.0.9 2.1.2 2.2.0-preview1-26618-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 3fbcc80189..ad704918df 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180807.2 -commithash:11495dbd236104434e08cb1152fcb58cf2a20923 +version:2.2.0-preview1-20180821.1 +commithash:c8d0cc52cd1abb697be24e288ffd54f8fae8bf17 From b3e163e44fa51de3388658b0f80c73e8692893c6 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Wed, 5 Sep 2018 16:34:00 -0700 Subject: [PATCH 37/57] Update branding to 2.2.0-preview3 --- version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.props b/version.props index f77be62107..761c13440a 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ 2.2.0 - preview2 + preview3 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 @@ -10,7 +10,7 @@ $(VersionSuffix)-$(BuildNumber) 0.5.0 - preview2 + preview3 $(ExperimentalVersionPrefix) $(ExperimentalVersionPrefix)-$(ExperimentalVersionSuffix)-final $(ExperimentalVersionSuffix)-$(BuildNumber) From f94ad0f2029243f116f7639e50a5c10ef63a63c5 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 9 Sep 2018 12:10:09 -0700 Subject: [PATCH 38/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 64 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index cd53ae3566..c3f2510f69 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,43 +3,43 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180821.1 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 - 2.2.0-preview2-35143 + 2.2.0-preview1-20180907.8 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 + 2.2.0-preview3-35202 2.0.9 - 2.1.2 - 2.2.0-preview1-26618-02 + 2.1.3 + 2.2.0-preview2-26905-02 15.6.1 4.7.49 2.0.3 11.0.2 - 4.5.1 + 4.6.0-preview2-26905-02 4.5.0 1.6.0 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index ad704918df..312f82f9a5 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180821.1 -commithash:c8d0cc52cd1abb697be24e288ffd54f8fae8bf17 +version:2.2.0-preview1-20180907.8 +commithash:078918eb5c1f176ee1da351c584fb4a4d7491aa0 From 3ac6439dcfde84a4861a5d728123e2a58d96bad4 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 16 Sep 2018 12:09:03 -0700 Subject: [PATCH 39/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index c3f2510f69..e80c82a59e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,35 +3,35 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180907.8 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 + 2.2.0-preview1-20180911.1 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 2.0.9 2.1.3 2.2.0-preview2-26905-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 312f82f9a5..7124f37441 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180907.8 -commithash:078918eb5c1f176ee1da351c584fb4a4d7491aa0 +version:2.2.0-preview1-20180911.1 +commithash:ddfecdfc6e8e4859db5a0daea578070b862aac65 From 4259b65c1619380a0fc14c1b906269fbf5ae0c82 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 19 Sep 2018 14:48:34 -0700 Subject: [PATCH 40/57] Use options for registering health checks (#479) * Use options for registering health checks This change pivots to use options for registering health checks. We get a few pretty nice things out of this, and it unblocks some of our requirements. Now all registration methods support the application developer configuring the name, failure-status, and tags for each health check. This is a requirment, that we weren't really satisfying - which is what led to this redesign. In support of this health checks now return pass/fail, and the service is responsible for assigning the status. ---- Health check authors that need configuration data (connection string as an example) now have three ways to do this depending on their requirements. 1. Create an instance and register that (easiest) 2. Use Type Activation and pass parameters (middle) 3. Use named options (richest) We expect most health checks to need/want some kind of configuration - which 1) works pretty well to solve. However many other health checks will need DI + configuration. It was also a gap that we didn't have a good way to use named options, when it's such a good fit for our scenarios. Added new registration methods for type activation that allow you to pass parameters for 2). Added a context type that allows the running health check access to its registration for 3). ---- Redesigned and renamed how status gets reported. Health checks return pass/fail result, but the overall HealthReport includes entries of a different type. This seems fine because there isn't really a way to consume a HealthCheckResult directly - the service is the only consumer. ---- Added support for tags. This was easy to add now that we have a separate registration type, and it's quite handy for building filters (see sample). * HARDER BETTER STRONGER FASTER --- .../HealthChecksSample/CustomWriterStartup.cs | 16 +- samples/HealthChecksSample/DBHealthStartup.cs | 2 +- .../DbConnectionHealthCheck.cs | 15 +- .../HealthChecksSample/GCInfoHealthCheck.cs | 59 +++- .../LivenessProbeStartup.cs | 13 +- .../SlowDependencyHealthCheck.cs | 8 +- .../SqlConnectionHealthCheck.cs | 9 +- .../HealthCheckMiddleware.cs | 6 +- .../HealthCheckOptions.cs | 18 +- .../HealthCheckResponseWriters.cs | 2 +- .../HealthCheckContext.cs | 13 + .../HealthCheckRegistration.cs | 132 +++++++++ .../HealthCheckResult.cs | 203 ++++---------- .../{HealthCheckStatus.cs => HealthStatus.cs} | 30 +- .../IHealthCheck.cs | 8 +- .../CompositeHealthCheckResult.cs | 66 ----- .../DefaultHealthCheckService.cs | 149 ++++++++++ ...{HealthCheck.cs => DelegateHealthCheck.cs} | 16 +- .../HealthCheckServiceCollectionExtensions.cs | 10 +- .../HealthChecksBuilder.cs | 33 +++ .../HealthChecksBuilderAddCheckExtensions.cs | 191 +++++++++++++ .../HealthChecksBuilderDelegateExtensions.cs | 169 ++++++++++++ .../IHealthChecksBuilder.cs | 24 ++ .../HealthCheckService.cs | 173 +++--------- .../HealthCheckServiceOptions.cs | 18 ++ .../HealthChecksBuilder.cs | 17 -- .../HealthChecksBuilderAddCheckExtensions.cs | 114 -------- .../HealthReport.cs | 60 ++++ .../HealthReportEntry.cs | 53 ++++ .../IHealthCheckService.cs | 60 ---- .../IHealthChecksBuilder.cs | 22 -- ...Extensions.Diagnostics.HealthChecks.csproj | 5 +- .../HealthCheckMiddlewareTests.cs | 47 ++-- .../CompositeHealthCheckResultTests.cs | 31 --- ...ts.cs => DefaultHealthCheckServiceTest.cs} | 178 +++++++----- .../HealthChecksBuilderTest.cs | 257 ++++++++++++++++++ .../ServiceCollectionExtensionsTest.cs} | 10 +- .../HealthChecksBuilderTests.cs | 33 --- .../HealthReportTest.cs | 31 +++ 39 files changed, 1478 insertions(+), 823 deletions(-) create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs rename src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/{HealthCheckStatus.cs => HealthStatus.cs} (53%) delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs rename src/Microsoft.Extensions.Diagnostics.HealthChecks/{HealthCheck.cs => DelegateHealthCheck.cs} (61%) rename src/Microsoft.Extensions.Diagnostics.HealthChecks/{ => DependencyInjection}/HealthCheckServiceCollectionExtensions.cs (68%) create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs delete mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs delete mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs rename test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/{HealthCheckServiceTests.cs => DefaultHealthCheckServiceTest.cs} (59%) create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs rename test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/{ServiceCollectionExtensionsTests.cs => DependencyInjection/ServiceCollectionExtensionsTest.cs} (76%) delete mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs diff --git a/samples/HealthChecksSample/CustomWriterStartup.cs b/samples/HealthChecksSample/CustomWriterStartup.cs index 0c3e542b72..6e37b1c6ff 100644 --- a/samples/HealthChecksSample/CustomWriterStartup.cs +++ b/samples/HealthChecksSample/CustomWriterStartup.cs @@ -17,14 +17,10 @@ namespace HealthChecksSample public void ConfigureServices(IServiceCollection services) { // Registers required services for health checks - services.AddHealthChecks(); - - // This is an example of registering a custom health check as a service. - // All IHealthCheck services will be available to the health check service and - // middleware. - // - // We recommend registering all health checks as Singleton services. - services.AddSingleton(); + services.AddHealthChecks() + + // Registers a custom health check implementation + .AddGCInfoCheck("GCInfo"); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) @@ -45,13 +41,13 @@ namespace HealthChecksSample }); } - private static Task WriteResponse(HttpContext httpContext, CompositeHealthCheckResult result) + private static Task WriteResponse(HttpContext httpContext, HealthReport result) { httpContext.Response.ContentType = "application/json"; var json = new JObject( new JProperty("status", result.Status.ToString()), - new JProperty("results", new JObject(result.Results.Select(pair => + new JProperty("results", new JObject(result.Entries.Select(pair => new JProperty(pair.Key, new JObject( new JProperty("status", pair.Value.Status.ToString()), new JProperty("description", pair.Value.Description), diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs index 58b6c8d157..43dd6abda6 100644 --- a/samples/HealthChecksSample/DBHealthStartup.cs +++ b/samples/HealthChecksSample/DBHealthStartup.cs @@ -22,7 +22,7 @@ namespace HealthChecksSample // Registers required services for health checks services.AddHealthChecks() // Add a health check for a SQL database - .AddCheck(new SqlConnectionHealthCheck("MyDatabase", Configuration["ConnectionStrings:DefaultConnection"])); + .AddCheck("MyDatabase", new SqlConnectionHealthCheck(Configuration["ConnectionStrings:DefaultConnection"])); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) diff --git a/samples/HealthChecksSample/DbConnectionHealthCheck.cs b/samples/HealthChecksSample/DbConnectionHealthCheck.cs index 00d9bd4803..5a0ddcdd55 100644 --- a/samples/HealthChecksSample/DbConnectionHealthCheck.cs +++ b/samples/HealthChecksSample/DbConnectionHealthCheck.cs @@ -11,20 +11,17 @@ namespace HealthChecksSample { public abstract class DbConnectionHealthCheck : IHealthCheck { - protected DbConnectionHealthCheck(string name, string connectionString) - : this(name, connectionString, testQuery: null) + protected DbConnectionHealthCheck(string connectionString) + : this(connectionString, testQuery: null) { } - protected DbConnectionHealthCheck(string name, string connectionString, string testQuery) + protected DbConnectionHealthCheck(string connectionString, string testQuery) { - Name = name ?? throw new System.ArgumentNullException(nameof(name)); ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); TestQuery = testQuery; } - public string Name { get; } - protected string ConnectionString { get; } // This sample supports specifying a query to run as a boolean test of whether the database @@ -36,7 +33,7 @@ namespace HealthChecksSample protected abstract DbConnection CreateConnection(string connectionString); - public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = CreateConnection(ConnectionString)) { @@ -54,11 +51,11 @@ namespace HealthChecksSample } catch (DbException ex) { - return HealthCheckResult.Unhealthy(ex); + return HealthCheckResult.Failed(exception: ex); } } - return HealthCheckResult.Healthy(); + return HealthCheckResult.Passed(); } } } diff --git a/samples/HealthChecksSample/GCInfoHealthCheck.cs b/samples/HealthChecksSample/GCInfoHealthCheck.cs index f37ade8980..20bcc1eb11 100644 --- a/samples/HealthChecksSample/GCInfoHealthCheck.cs +++ b/samples/HealthChecksSample/GCInfoHealthCheck.cs @@ -2,21 +2,58 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; namespace HealthChecksSample { // This is an example of a custom health check that implements IHealthCheck. // - // See CustomWriterStartup to see how this is registered. + // This example also shows a technique for authoring a health check that needs to be registered + // with additional configuration data. This technique works via named options, and is useful + // for authoring health checks that can be disctributed as libraries. + + public static class GCInfoHealthCheckBuilderExtensions + { + public static IHealthChecksBuilder AddGCInfoCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus = null, + IEnumerable tags = null, + long? thresholdInBytes = null) + { + // Register a check of type GCInfo + builder.AddCheck(name, failureStatus ?? HealthStatus.Degraded, tags); + + // Configure named options to pass the threshold into the check. + if (thresholdInBytes.HasValue) + { + builder.Services.Configure(name, options => + { + options.Threshold = thresholdInBytes.Value; + }); + } + + return builder; + } + } + public class GCInfoHealthCheck : IHealthCheck { - public string Name { get; } = "GCInfo"; + private readonly IOptionsMonitor _options; - public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public GCInfoHealthCheck(IOptionsMonitor options) { + _options = options; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) + { + var options = _options.Get(context.Registration.Name); + // This example will report degraded status if the application is using - // more than 1gb of memory. + // more than the configured amount of memory (1gb by default). // // Additionally we include some GC info in the reported diagnostics. var allocated = GC.GetTotalMemory(forceFullCollection: false); @@ -28,14 +65,20 @@ namespace HealthChecksSample { "Gen2Collections", GC.CollectionCount(2) }, }; - // Report degraded status if the allocated memory is >= 1gb (in bytes) - var status = allocated >= 1024 * 1024 * 1024 ? HealthCheckStatus.Degraded : HealthCheckStatus.Healthy; + // Report failure if the allocated memory is >= the threshold + var result = allocated >= options.Threshold; return Task.FromResult(new HealthCheckResult( - status, - exception: null, + result, description: "reports degraded status if allocated bytes >= 1gb", + exception: null, data: data)); } } + + public class GCInfoOptions + { + // The failure threshold (in bytes) + public long Threshold { get; set; } = 1024L * 1024L * 1024L; + } } diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs index aa644d231f..82676221ce 100644 --- a/samples/HealthChecksSample/LivenessProbeStartup.cs +++ b/samples/HealthChecksSample/LivenessProbeStartup.cs @@ -1,11 +1,9 @@ using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthChecksSample { @@ -17,7 +15,7 @@ namespace HealthChecksSample // Registers required services for health checks services .AddHealthChecks() - .AddCheck(new SlowDependencyHealthCheck()); + .AddCheck("Slow", failureStatus: null, tags: new[] { "ready", }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) @@ -46,10 +44,13 @@ namespace HealthChecksSample // long initialization time (15 seconds). - // The readiness check uses all of the registered health checks (default) - app.UseHealthChecks("/health/ready"); + // The readiness check uses all registered checks with the 'ready' tag. + app.UseHealthChecks("/health/ready", new HealthCheckOptions() + { + Predicate = (check) => check.Tags.Contains("ready"), + }); - // The liveness check uses an 'identity' health check that always returns healthy + // The liveness filters out all checks and just returns success app.UseHealthChecks("/health/live", new HealthCheckOptions() { // Exclude all checks, just return a 200. diff --git a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs index 7832724c81..e319952cf0 100644 --- a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs +++ b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs @@ -17,16 +17,14 @@ namespace HealthChecksSample _task = Task.Delay(15 * 1000); } - public string Name => HealthCheckName; - - public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { if (_task.IsCompleted) { - return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready")); + return Task.FromResult(HealthCheckResult.Passed("Dependency is ready")); } - return Task.FromResult(HealthCheckResult.Unhealthy("Dependency is still initializing")); + return Task.FromResult(HealthCheckResult.Failed("Dependency is still initializing")); } } } diff --git a/samples/HealthChecksSample/SqlConnectionHealthCheck.cs b/samples/HealthChecksSample/SqlConnectionHealthCheck.cs index 5849da2202..fcc135ad1e 100644 --- a/samples/HealthChecksSample/SqlConnectionHealthCheck.cs +++ b/samples/HealthChecksSample/SqlConnectionHealthCheck.cs @@ -1,6 +1,5 @@ using System.Data.Common; using System.Data.SqlClient; -using Microsoft.Extensions.Diagnostics.HealthChecks; namespace HealthChecksSample { @@ -8,13 +7,13 @@ namespace HealthChecksSample { private static readonly string DefaultTestQuery = "Select 1"; - public SqlConnectionHealthCheck(string name, string connectionString) - : this(name, connectionString, testQuery: DefaultTestQuery) + public SqlConnectionHealthCheck(string connectionString) + : this(connectionString, testQuery: DefaultTestQuery) { } - public SqlConnectionHealthCheck(string name, string connectionString, string testQuery) - : base(name, connectionString, testQuery ?? DefaultTestQuery) + public SqlConnectionHealthCheck(string connectionString, string testQuery) + : base(connectionString, testQuery ?? DefaultTestQuery) { } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 20ec5e356e..e06ef8509b 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -15,12 +15,12 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { private readonly RequestDelegate _next; private readonly HealthCheckOptions _healthCheckOptions; - private readonly IHealthCheckService _healthCheckService; + private readonly HealthCheckService _healthCheckService; public HealthCheckMiddleware( RequestDelegate next, IOptions healthCheckOptions, - IHealthCheckService healthCheckService) + HealthCheckService healthCheckService) { if (next == null) { @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode)) { var message = - $"No status code mapping found for {nameof(HealthCheckStatus)} value: {result.Status}." + + $"No status code mapping found for {nameof(HealthStatus)} value: {result.Status}." + $"{nameof(HealthCheckOptions)}.{nameof(HealthCheckOptions.ResultStatusCodes)} must contain" + $"an entry for {result.Status}."; diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 7930d25577..42996e3701 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -22,20 +22,20 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// registered health checks - this is the default behavior. To run a subset of health checks, /// provide a function that filters the set of checks. /// - public Func Predicate { get; set; } + public Func Predicate { get; set; } /// - /// Gets a dictionary mapping the to an HTTP status code applied to the response. + /// Gets a dictionary mapping the to an HTTP status code applied to the response. /// This property can be used to configure the status codes returned for each status. /// - public IDictionary ResultStatusCodes { get; } = new Dictionary() + public IDictionary ResultStatusCodes { get; } = new Dictionary() { - { HealthCheckStatus.Healthy, StatusCodes.Status200OK }, - { HealthCheckStatus.Degraded, StatusCodes.Status200OK }, - { HealthCheckStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, + { HealthStatus.Healthy, StatusCodes.Status200OK }, + { HealthStatus.Degraded, StatusCodes.Status200OK }, + { HealthStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, // This means that a health check failed, so 500 is appropriate. This is an error. - { HealthCheckStatus.Failed, StatusCodes.Status500InternalServerError }, + { HealthStatus.Failed, StatusCodes.Status500InternalServerError }, }; /// @@ -43,8 +43,8 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// /// /// The default value is a delegate that will write a minimal text/plain response with the value - /// of as a string. + /// of as a string. /// - public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; + public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs index a74072f63f..50cb8c736e 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckResponseWriters.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { internal static class HealthCheckResponseWriters { - public static Task WriteMinimalPlaintext(HttpContext httpContext, CompositeHealthCheckResult result) + public static Task WriteMinimalPlaintext(HttpContext httpContext, HealthReport result) { httpContext.Response.ContentType = "text/plain"; return httpContext.Response.WriteAsync(result.Status.ToString()); diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs new file mode 100644 index 0000000000..027451c0d2 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckContext.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public sealed class HealthCheckContext + { + /// + /// Gets or sets the of the currently executing . + /// + public HealthCheckRegistration Registration { get; set; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs new file mode 100644 index 0000000000..9291c38846 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckRegistration.cs @@ -0,0 +1,132 @@ +// 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. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represent the registration information associated with an implementation. + /// + /// + /// + /// The health check registration is provided as a separate object so that application developers can customize + /// how health check implementations are configured. + /// + /// + /// The registration is provided to an implementation during execution through + /// . This allows a health check implementation to access named + /// options or perform other operations based on the registered name. + /// + /// + public sealed class HealthCheckRegistration + { + private Func _factory; + private string _name; + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// The instance. + /// + /// The that should be reported upon failure of the health check. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + Name = name; + FailureStatus = failureStatus ?? HealthStatus.Unhealthy; + Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + Factory = (_) => instance; + } + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// A delegate used to create the instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + public HealthCheckRegistration( + string name, + Func factory, + HealthStatus? failureStatus, + IEnumerable tags) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + Name = name; + FailureStatus = failureStatus ?? HealthStatus.Unhealthy; + Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + Factory = factory; + } + + /// + /// Gets or sets a delegate used to create the instance. + /// + public Func Factory + { + get => _factory; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _factory = value; + } + } + + /// + /// Gets or sets the that should be reported upon failure of the health check. + /// + public HealthStatus FailureStatus { get; set; } + + /// + /// Gets or sets the health check name. + /// + public string Name + { + get => _name; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _name = value; + } + } + + /// + /// Gets a list of tags that can be used for filtering health checks. + /// + public ISet Tags { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs index e672c88db5..12cd38d08e 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs @@ -13,176 +13,65 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { private static readonly IReadOnlyDictionary _emptyReadOnlyDictionary = new Dictionary(); - private string _description; - private IReadOnlyDictionary _data; - /// - /// Gets a value indicating the status of the component that was checked. + /// Creates a new with the specified values for , , + /// , and . /// - public HealthCheckStatus Status { get; } - - /// - /// Gets an representing the exception that was thrown when checking for status (if any). - /// - /// - /// This value is expected to be 'null' if is . - /// - public Exception Exception { get; } - - /// - /// Gets a human-readable description of the status of the component that was checked. - /// - public string Description => _description ?? string.Empty; + /// A value indicating the pass/fail status of the component that was checked. + /// A human-readable description of the status of the component that was checked. + /// An representing the exception that was thrown when checking for status (if any). + /// Additional key-value pairs describing the health of the component. + public HealthCheckResult(bool result, string description, Exception exception, IReadOnlyDictionary data) + { + Result = result; + Description = description; + Exception = exception; + Data = data ?? _emptyReadOnlyDictionary; + } /// /// Gets additional key-value pairs describing the health of the component. /// - public IReadOnlyDictionary Data => _data ?? _emptyReadOnlyDictionary; + public IReadOnlyDictionary Data { get; } /// - /// Creates a new with the specified , , - /// , and . + /// Gets a human-readable description of the status of the component that was checked. /// - /// A value indicating the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public HealthCheckResult(HealthCheckStatus status, Exception exception, string description, IReadOnlyDictionary data) - { - if (status == HealthCheckStatus.Unknown) - { - throw new ArgumentException($"'{nameof(HealthCheckStatus.Unknown)}' is not a valid value for the 'status' parameter.", nameof(status)); - } + public string Description { get; } - Status = status; - Exception = exception; - _description = description; - _data = data; + /// + /// Gets an representing the exception that was thrown when checking for status (if any). + /// + public Exception Exception { get; } + + /// + /// Gets a value indicating the pass/fail status of the component that was checked. If true, then the component + /// is considered to have passed health validation. A false value will be mapped to the configured + /// by the health check system. + /// + public bool Result { get; } + + /// + /// Creates a representing a passing component. + /// + /// A representing a passing component. + /// A human-readable description of the status of the component that was checked. Optional. + /// Additional key-value pairs describing the health of the component. Optional. + public static HealthCheckResult Passed(string description = null, IReadOnlyDictionary data = null) + { + return new HealthCheckResult(result: true, description, exception: null, data); } /// - /// Creates a representing an unhealthy component. + /// Creates a representing an failing component. /// - /// A representing an unhealthy component. - public static HealthCheckResult Unhealthy() - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Unhealthy(string description) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Unhealthy(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: data); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Unhealthy(Exception exception) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description: string.Empty, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Unhealthy(string description, Exception exception) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data: null); - - /// - /// Creates a representing an unhealthy component. - /// - /// A representing an unhealthy component. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Unhealthy(string description, Exception exception, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - public static HealthCheckResult Healthy() - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Healthy(string description) - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: null); - - /// - /// Creates a representing a healthy component. - /// - /// A representing a healthy component. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Healthy(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: data); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - public static HealthCheckResult Degraded() - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - public static HealthCheckResult Degraded(string description) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Degraded(string description, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: data); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - public static HealthCheckResult Degraded(Exception exception) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - public static HealthCheckResult Degraded(string description, Exception exception) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data: null); - - /// - /// Creates a representing a component in a degraded state. - /// - /// A representing a component in a degraded state. - /// A human-readable description of the status of the component that was checked. - /// An representing the exception that was thrown when checking for status (if any). - /// Additional key-value pairs describing the health of the component. - public static HealthCheckResult Degraded(string description, Exception exception, IReadOnlyDictionary data) - => new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data); + /// A human-readable description of the status of the component that was checked. Optional. + /// An representing the exception that was thrown when checking for status. Optional. + /// Additional key-value pairs describing the health of the component. Optional. + /// A representing an failing component. + public static HealthCheckResult Failed(string description = null, Exception exception = null, IReadOnlyDictionary data = null) + { + return new HealthCheckResult(result: false, description, exception, data); + } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs similarity index 53% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs index c16739eadc..d4293cb7b4 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckStatus.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs @@ -4,38 +4,38 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { /// - /// Represents the status of a health check result. + /// Represents the reported status of a health check result. /// /// - /// The values of this enum or ordered from least healthy to most healthy. So is - /// greater than but less than . + /// + /// A health status is derived the pass/fail result of an () + /// and the corresponding value of . + /// + /// + /// The values of this enum or ordered from least healthy to most healthy. So is + /// greater than but less than . + /// /// - public enum HealthCheckStatus + public enum HealthStatus { /// - /// This value should not be returned by a health check. It is used to represent an uninitialized value. + /// Indicates that an unexpected exception was thrown when running the health check. /// - Unknown = 0, - - /// - /// This value should not be returned by a health check. It is used to indicate that an unexpected exception was - /// thrown when running the health check. - /// - Failed = 1, + Failed = 0, /// /// Indicates that the health check determined that the component was unhealthy. /// - Unhealthy = 2, + Unhealthy = 1, /// /// Indicates that the health check determined that the component was in a degraded state. /// - Degraded = 3, + Degraded = 2, /// /// Indicates that the health check determined that the component was healthy. /// - Healthy = 4, + Healthy = 3, } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs index 2bf751180d..1b69953b67 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheck.cs @@ -12,16 +12,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// public interface IHealthCheck { - /// - /// Gets the name of the health check, which should indicate the component being checked. - /// - string Name { get; } - /// /// Runs the health check, returning the status of the component being checked. /// + /// A context object associated with the current execution. /// A that can be used to cancel the health check. /// A that completes when the health check has finished, yielding the status of the component being checked. - Task CheckHealthAsync(CancellationToken cancellationToken = default); + Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs deleted file mode 100644 index 900b15bd71..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/CompositeHealthCheckResult.cs +++ /dev/null @@ -1,66 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - /// - /// Represents the results of multiple health checks. - /// - public class CompositeHealthCheckResult - { - /// - /// A containing the results from each health check. - /// - /// - /// The keys in this dictionary map to the name of the health check, the values are the - /// returned when was called for that health check. - /// - public IReadOnlyDictionary Results { get; } - - /// - /// Gets a representing the aggregate status of all the health checks. - /// - /// - /// This value is determined by taking the "worst" result of all the results. So if any result is , - /// this value is . If no result is but any result is - /// , this value is , etc. - /// - public HealthCheckStatus Status { get; } - - /// - /// Create a new from the specified results. - /// - /// A containing the results from each health check. - public CompositeHealthCheckResult(IReadOnlyDictionary results) - { - Results = results; - Status = CalculateAggregateStatus(results.Values); - } - - private HealthCheckStatus CalculateAggregateStatus(IEnumerable results) - { - // This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list - var currentValue = HealthCheckStatus.Healthy; - foreach (var result in results) - { - if (currentValue > result.Status) - { - currentValue = result.Status; - } - - if (currentValue == HealthCheckStatus.Failed) - { - // Game over, man! Game over! - // (We hit the worst possible status, so there's no need to keep iterating) - return currentValue; - } - } - - return currentValue; - } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs new file mode 100644 index 0000000000..b7a34ff35a --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -0,0 +1,149 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + internal class DefaultHealthCheckService : HealthCheckService + { + private readonly IServiceScopeFactory _scopeFactory; + private readonly IOptions _options; + private readonly ILogger _logger; + + public DefaultHealthCheckService( + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger) + { + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + // 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 + // actually tries to **run** health checks would be real baaaaad. + ValidateRegistrations(_options.Value.Registrations); + } + public override async Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default) + { + var registrations = _options.Value.Registrations; + + using (var scope = _scopeFactory.CreateScope()) + { + var context = new HealthCheckContext(); + var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var registration in registrations) + { + if (predicate != null && !predicate(registration)) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var healthCheck = registration.Factory(scope.ServiceProvider); + + // If the health check does things like make Database queries using EF or backend HTTP calls, + // it may be valuable to know that logs it generates are part of a health check. So we start a scope. + using (_logger.BeginScope(new HealthCheckLogScope(registration.Name))) + { + var stopwatch = ValueStopwatch.StartNew(); + context.Registration = registration; + + Log.HealthCheckBegin(_logger, registration); + + HealthReportEntry entry; + try + { + var result = await healthCheck.CheckHealthAsync(context, cancellationToken); + + entry = new HealthReportEntry( + result.Result ? HealthStatus.Healthy : registration.FailureStatus, + result.Description, + result.Exception, + result.Data); + + Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime()); + } + catch (Exception ex) + { + entry = new HealthReportEntry(HealthStatus.Failed, ex.Message, ex, data: null); + Log.HealthCheckError(_logger, registration, ex, stopwatch.GetElapsedTime()); + } + + entries[registration.Name] = entry; + } + } + + return new HealthReport(entries); + } + } + + private static void ValidateRegistrations(IEnumerable registrations) + { + // Scan the list for duplicate names to provide a better error if there are duplicates. + var duplicateNames = registrations + .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + if (duplicateNames.Count > 0) + { + throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(registrations)); + } + } + + private static class Log + { + public static class EventIds + { + public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); + public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); + public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError"); + } + + private static readonly Action _healthCheckBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckBegin, + "Running health check {HealthCheckName}"); + + private static readonly Action _healthCheckEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckEnd, + "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}"); + + private static readonly Action _healthCheckError = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckError, + "Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms"); + + public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration) + { + _healthCheckBegin(logger, registration.Name, null); + } + + public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration) + { + _healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, null); + } + + public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration) + { + _healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception); + } + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs similarity index 61% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs index da6a92d062..94069fd7d1 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DelegateHealthCheck.cs @@ -11,31 +11,25 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// A simple implementation of which uses a provided delegate to /// implement the check. /// - public sealed class HealthCheck : IHealthCheck + internal sealed class DelegateHealthCheck : IHealthCheck { private readonly Func> _check; /// - /// Create an instance of from the specified and . + /// Create an instance of from the specified delegate. /// - /// The name of the health check, which should indicate the component being checked. /// A delegate which provides the code to execute when the health check is run. - public HealthCheck(string name, Func> check) + public DelegateHealthCheck(Func> check) { - Name = name ?? throw new ArgumentNullException(nameof(name)); _check = check ?? throw new ArgumentNullException(nameof(check)); } - /// - /// Gets the name of the health check, which should indicate the component being checked. - /// - public string Name { get; } - /// /// Runs the health check, returning the status of the component being checked. /// + /// A context object associated with the current execution. /// A that can be used to cancel the health check. /// A that completes when the health check has finished, yielding the status of the component being checked. - public Task CheckHealthAsync(CancellationToken cancellationToken = default) => _check(cancellationToken); + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) => _check(cancellationToken); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs similarity index 68% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs index f0caa8db1f..76b0dc45c1 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceCollectionExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs @@ -7,24 +7,24 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Microsoft.Extensions.DependencyInjection { /// - /// Provides extension methods for registering in an . + /// Provides extension methods for registering in an . /// public static class HealthCheckServiceCollectionExtensions { /// - /// Adds the to the container, using the provided delegate to register + /// Adds the to the container, using the provided delegate to register /// health checks. /// /// /// This operation is idempotent - multiple invocations will still only result in a single - /// instance in the . It can be invoked + /// instance in the . It can be invoked /// multiple times in order to get access to the in multiple places. /// - /// The to add the to. + /// The to add the to. /// An instance of from which health checks can be registered. public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); return new HealthChecksBuilder(services); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs new file mode 100644 index 0000000000..231dd51717 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilder.cs @@ -0,0 +1,33 @@ +// 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. + +using System; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + internal class HealthChecksBuilder : IHealthChecksBuilder + { + public HealthChecksBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + + public IHealthChecksBuilder Add(HealthCheckRegistration registration) + { + if (registration == null) + { + throw new ArgumentNullException(nameof(registration)); + } + + Services.Configure(options => + { + options.Registrations.Add(registration); + }); + + return this; + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs new file mode 100644 index 0000000000..9508889054 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs @@ -0,0 +1,191 @@ +// 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. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Provides basic extension methods for registering instances in an . + /// + public static class HealthChecksBuilderAddCheckExtensions + { + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// An instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + IHealthCheck instance, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. If a service of type is registred in the dependency injection container + /// with any liftime it will be used. Otherwise an instance of type will be constructed with + /// access to services from the dependency injection container. + /// + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus = null, + IEnumerable tags = null) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance(s), failureStatus, tags)); + } + + // NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't + // play super well with params. + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck(this IHealthChecksBuilder builder, string name, params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return AddTypeActivatedCheck(builder, name, failureStatus: null, tags: null); + } + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return AddTypeActivatedCheck(builder, name, failureStatus, tags: null); + } + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// Additional arguments to provide to the constructor. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + IEnumerable tags, + params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance(s, args), failureStatus, tags)); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs new file mode 100644 index 0000000000..70edd0649d --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs @@ -0,0 +1,169 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Provides extension methods for registering delegates with the . + /// + public static class HealthChecksBuilderDelegateExtensions + { + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => Task.FromResult(check())); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct))); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => check()); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + HealthStatus? failureStatus = null, + IEnumerable tags = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (check == null) + { + throw new ArgumentNullException(nameof(check)); + } + + var instance = new DelegateHealthCheck((ct) => check(ct)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs new file mode 100644 index 0000000000..eb78293f87 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/IHealthChecksBuilder.cs @@ -0,0 +1,24 @@ +// 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. + +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// A builder used to register health checks. + /// + public interface IHealthChecksBuilder + { + /// + /// Adds a for a health check. + /// + /// The . + IHealthChecksBuilder Add(HealthCheckRegistration registration); + + /// + /// Gets the into which instances should be registered. + /// + IServiceCollection Services { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs index 111113ea77..e4a128148d 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs @@ -2,143 +2,60 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Internal; -using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.Diagnostics.HealthChecks { - internal class HealthCheckService : IHealthCheckService + /// + /// A service which can be used to check the status of instances + /// registered in the application. + /// + /// + /// + /// The default implementation of is registered in the dependency + /// injection container as a singleton service by calling + /// . + /// + /// + /// The returned by + /// + /// provides a convenience API for registering health checks. + /// + /// + /// implementations can be registered through extension methods provided by + /// . + /// + /// + public abstract class HealthCheckService { - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - - public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger logger) + /// + /// Runs all the health checks in the application and returns the aggregated status. + /// + /// A which can be used to cancel the health checks. + /// + /// A which will complete when all the health checks have been run, + /// yielding a containing the results. + /// + public Task CheckHealthAsync(CancellationToken cancellationToken = default) { - _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - // 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 - // actually tries to **run** health checks would be real baaaaad. - using (var scope = _scopeFactory.CreateScope()) - { - var healthChecks = scope.ServiceProvider.GetRequiredService>(); - EnsureNoDuplicates(healthChecks); - } + return CheckHealthAsync(predicate: null, cancellationToken); } - public Task CheckHealthAsync(CancellationToken cancellationToken = default) => - CheckHealthAsync(predicate: null, cancellationToken); - - public async Task CheckHealthAsync( - Func predicate, - CancellationToken cancellationToken = default) - { - using (var scope = _scopeFactory.CreateScope()) - { - var healthChecks = scope.ServiceProvider.GetRequiredService>(); - - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var healthCheck in healthChecks) - { - if (predicate != null && !predicate(healthCheck)) - { - continue; - } - - cancellationToken.ThrowIfCancellationRequested(); - - // If the health check does things like make Database queries using EF or backend HTTP calls, - // it may be valuable to know that logs it generates are part of a health check. So we start a scope. - using (_logger.BeginScope(new HealthCheckLogScope(healthCheck.Name))) - { - HealthCheckResult result; - try - { - Log.HealthCheckBegin(_logger, healthCheck); - var stopwatch = ValueStopwatch.StartNew(); - result = await healthCheck.CheckHealthAsync(cancellationToken); - Log.HealthCheckEnd(_logger, healthCheck, result, stopwatch.GetElapsedTime()); - } - catch (Exception ex) - { - Log.HealthCheckError(_logger, healthCheck, ex); - result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null); - } - - // This can only happen if the result is default(HealthCheckResult) - if (result.Status == HealthCheckStatus.Unknown) - { - // This is different from the case above. We throw here because a health check is doing something specifically incorrect. - throw new InvalidOperationException($"Health check '{healthCheck.Name}' returned a result with a status of Unknown"); - } - - results[healthCheck.Name] = result; - } - } - - return new CompositeHealthCheckResult(results); - } - } - - private static void EnsureNoDuplicates(IEnumerable healthChecks) - { - // Scan the list for duplicate names to provide a better error if there are duplicates. - var duplicateNames = healthChecks - .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) - .Where(g => g.Count() > 1) - .Select(g => g.Key) - .ToList(); - - if (duplicateNames.Count > 0) - { - throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(healthChecks)); - } - } - - private static class Log - { - public static class EventIds - { - public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); - public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); - public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError"); - } - - private static readonly Action _healthCheckBegin = LoggerMessage.Define( - LogLevel.Debug, - EventIds.HealthCheckBegin, - "Running health check {HealthCheckName}"); - - private static readonly Action _healthCheckEnd = LoggerMessage.Define( - LogLevel.Debug, - EventIds.HealthCheckEnd, - "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}"); - - private static readonly Action _healthCheckError = LoggerMessage.Define( - LogLevel.Error, - EventIds.HealthCheckError, - "Health check {HealthCheckName} threw an unhandled exception"); - - public static void HealthCheckBegin(ILogger logger, IHealthCheck healthCheck) - { - _healthCheckBegin(logger, healthCheck.Name, null); - } - - public static void HealthCheckEnd(ILogger logger, IHealthCheck healthCheck, HealthCheckResult result, TimeSpan duration) - { - _healthCheckEnd(logger, healthCheck.Name, duration.TotalMilliseconds, result.Status, null); - } - - public static void HealthCheckError(ILogger logger, IHealthCheck healthCheck, Exception exception) - { - _healthCheckError(logger, healthCheck.Name, exception); - } - } + /// + /// Runs the provided health checks and returns the aggregated status + /// + /// + /// A predicate that can be used to include health checks based on user-defined criteria. + /// + /// A which can be used to cancel the health checks. + /// + /// A which will complete when all the health checks have been run, + /// yielding a containing the results. + /// + public abstract Task CheckHealthAsync( + Func predicate, + CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs new file mode 100644 index 0000000000..b8dfdb9b40 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckServiceOptions.cs @@ -0,0 +1,18 @@ +// 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. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Options for the default implementation of + /// + public sealed class HealthCheckServiceOptions + { + /// + /// Gets the health check registrations. + /// + public ICollection Registrations { get; } = new List(); + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs deleted file mode 100644 index 4e1d851eff..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - internal class HealthChecksBuilder : IHealthChecksBuilder - { - public IServiceCollection Services { get; } - - public HealthChecksBuilder(IServiceCollection services) - { - Services = services; - } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs deleted file mode 100644 index d3af5b17c2..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -// 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. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Provides extension methods for registering instances in an . - /// - public static class HealthChecksBuilderAddCheckExtensions - { - /// - /// Adds a new health check with the specified name and implementation. - /// - /// The to add the check to. - /// The name of the health check, which should indicate the component being checked. - /// A delegate which provides the code to execute when the health check is run. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - return builder.AddCheck(new HealthCheck(name, check)); - } - - /// - /// Adds a new health check with the specified name and implementation. - /// - /// The to add the check to. - /// The name of the health check, which should indicate the component being checked. - /// A delegate which provides the code to execute when the health check is run. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func> check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - return builder.AddCheck(name, _ => check()); - } - - /// - /// Adds a new health check with the provided implementation. - /// - /// The to add the check to. - /// An implementation. - /// The . - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, IHealthCheck check) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (check == null) - { - throw new ArgumentNullException(nameof(check)); - } - - builder.Services.AddSingleton(check); - return builder; - } - - /// - /// Adds a new health check as a transient dependency injected service with the provided type. - /// - /// The health check implementation type. - /// The . - /// The . - /// - /// This method will register a transient service of type with the - /// provided implementation type . Using this method to register a health - /// check allows you to register a health check that depends on transient and scoped services. - /// - public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder) where T : class, IHealthCheck - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - builder.Services.Add(ServiceDescriptor.Transient(typeof(IHealthCheck), typeof(T))); - return builder; - } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs new file mode 100644 index 0000000000..29359a547f --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs @@ -0,0 +1,60 @@ +// 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. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represents the result of executing a group of instances. + /// + public sealed class HealthReport + { + /// + /// Create a new from the specified results. + /// + /// A containing the results from each health check. + public HealthReport(IReadOnlyDictionary entries) + { + Entries = entries; + Status = CalculateAggregateStatus(entries.Values); + } + + /// + /// A containing the results from each health check. + /// + /// + /// The keys in this dictionary map the name of each executed health check to a for the + /// result data retruned from the corresponding health check. + /// + public IReadOnlyDictionary Entries { get; } + + /// + /// Gets a representing the aggregate status of all the health checks. The value of + /// will be the most servere status reported by a health check. If no checks were executed, the value is always . + /// + public HealthStatus Status { get; } + + private HealthStatus CalculateAggregateStatus(IEnumerable entries) + { + // This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list + var currentValue = HealthStatus.Healthy; + foreach (var entry in entries) + { + if (currentValue > entry.Status) + { + currentValue = entry.Status; + } + + if (currentValue == HealthStatus.Failed) + { + // Game over, man! Game over! + // (We hit the worst possible status, so there's no need to keep iterating) + return currentValue; + } + } + + return currentValue; + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs new file mode 100644 index 0000000000..17ed5ae288 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs @@ -0,0 +1,53 @@ +// 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. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represents an entry in a . Corresponds to the result of a single . + /// + public struct HealthReportEntry + { + private static readonly IReadOnlyDictionary _emptyReadOnlyDictionary = new Dictionary(); + + /// + /// Creates a new with the specified values for , , + /// , and . + /// + /// A value indicating the health status of the component that was checked. + /// A human-readable description of the status of the component that was checked. + /// An representing the exception that was thrown when checking for status (if any). + /// Additional key-value pairs describing the health of the component. + public HealthReportEntry(HealthStatus status, string description, Exception exception, IReadOnlyDictionary data) + { + Status = status; + Description = description; + Exception = exception; + Data = data ?? _emptyReadOnlyDictionary; + } + + /// + /// Gets additional key-value pairs describing the health of the component. + /// + public IReadOnlyDictionary Data { get; } + + /// + /// Gets a human-readable description of the status of the component that was checked. + /// + public string Description { get; } + + /// + /// Gets an representing the exception that was thrown when checking for status (if any). + /// + public Exception Exception { get; } + + /// + /// Gets the health status of the component that was checked. The is based on the pass/fail value of + /// and the configured value of . + /// + public HealthStatus Status { get; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs deleted file mode 100644 index f931475a4d..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs +++ /dev/null @@ -1,60 +0,0 @@ -// 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. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - /// - /// A service which can be used to check the status of instances - /// registered in the application. - /// - /// - /// - /// The default implementation of is registered in the dependency - /// injection container as a singleton service by calling - /// . - /// - /// - /// The returned by - /// - /// provides a convenience API for registering health checks. - /// - /// - /// The default implementation of will use all services - /// of type registered in the dependency injection container. - /// implementations may be registered with any service lifetime. The implementation will create a scope - /// for each aggregate health check operation and use the scope to resolve services. The scope - /// created for executing health checks is controlled by the health checks service and does not - /// share scoped services with any other scope in the application. - /// - /// - public interface IHealthCheckService - { - /// - /// Runs all the health checks in the application and returns the aggregated status. - /// - /// A which can be used to cancel the health checks. - /// - /// A which will complete when all the health checks have been run, - /// yielding a containing the results. - /// - Task CheckHealthAsync(CancellationToken cancellationToken = default); - - /// - /// Runs the provided health checks and returns the aggregated status - /// - /// - /// A predicate that can be used to include health checks based on user-defined criteria. - /// - /// A which can be used to cancel the health checks. - /// - /// A which will complete when all the health checks have been run, - /// yielding a containing the results. - /// - Task CheckHealthAsync(Func predicate, - CancellationToken cancellationToken = default); - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs deleted file mode 100644 index 22cd691aa2..0000000000 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthChecksBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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. - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - /// - /// A builder used to collect instances of and register them on an . - /// - /// - /// This type wraps an and provides a place for health check components to attach extension - /// methods for registering themselves in the . - /// - public interface IHealthChecksBuilder - { - /// - /// Gets the into which instances should be registered. - /// - IServiceCollection Services { get; } - } -} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index 2b3d0e6261..725226023e 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -3,8 +3,8 @@ Components for performing health checks in .NET applications Commonly Used Types: -Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService -Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder +Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService +Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder netstandard2.0 $(NoWarn);CS1591 @@ -14,6 +14,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder + diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 16fe68ac2a..7a2435948c 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -1,8 +1,6 @@ // 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. -using System; -using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -100,7 +98,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } - [Fact] public async Task StatusCodeIs200IfAllChecksHealthy() { @@ -112,9 +109,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Bar", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!")); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -137,9 +134,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Not so great."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!")) + .AddCheck("Bar", () => HealthCheckResult.Failed("Not so great."), failureStatus: HealthStatus.Degraded) + .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!")); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -162,9 +159,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -187,9 +184,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(new HealthCheckResult(HealthCheckStatus.Failed, null, null, null))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => throw null) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -225,9 +222,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -254,9 +251,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -277,7 +274,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { ResultStatusCodes = { - [HealthCheckStatus.Healthy] = 201, + [HealthStatus.Healthy] = 201, } }); }) @@ -308,10 +305,10 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) // Will get filtered out - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) - .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("A-ok!"))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs deleted file mode 100644 index 3852969cda..0000000000 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/CompositeHealthCheckResultTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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. - -using System.Collections.Generic; -using Xunit; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks.Tests -{ - public class CompositeHealthCheckResultTests - { - [Theory] - [InlineData(HealthCheckStatus.Healthy)] - [InlineData(HealthCheckStatus.Degraded)] - [InlineData(HealthCheckStatus.Unhealthy)] - [InlineData(HealthCheckStatus.Failed)] - public void Status_MatchesWorstStatusInResults(HealthCheckStatus statusValue) - { - var result = new CompositeHealthCheckResult(new Dictionary() - { - {"Foo", HealthCheckResult.Healthy() }, - {"Bar", HealthCheckResult.Healthy() }, - {"Baz", new HealthCheckResult(statusValue, exception: null, description: null, data: null) }, - {"Quick", HealthCheckResult.Healthy() }, - {"Quack", HealthCheckResult.Healthy() }, - {"Quock", HealthCheckResult.Healthy() }, - }); - - Assert.Equal(statusValue, result.Status); - } - } -} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs similarity index 59% rename from test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs rename to test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs index 6e7c28e9ef..024309a024 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs @@ -8,11 +8,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.Extensions.Diagnostics.HealthChecks { - public class HealthCheckServiceTests + public class DefaultHealthCheckServiceTest { [Fact] public void Constructor_ThrowsUsefulExceptionForDuplicateNames() @@ -25,22 +26,23 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks serviceCollection.AddLogging(); serviceCollection.AddOptions(); serviceCollection.AddHealthChecks() - .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) - .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()))) - .AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy()))) - .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))) - .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()))); + .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) + .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))); var services = serviceCollection.BuildServiceProvider(); - + var scopeFactory = services.GetRequiredService(); - var logger = services.GetRequiredService>(); + var options = services.GetRequiredService>(); + var logger = services.GetRequiredService>(); // Act - var exception = Assert.Throws(() => new HealthCheckService(scopeFactory, logger)); + var exception = Assert.Throws(() => new DefaultHealthCheckService(scopeFactory, options, logger)); // Assert - Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message); + Assert.StartsWith($"Duplicate health checks were registered with the name(s): Foo, Baz", exception.Message); } [Fact] @@ -61,21 +63,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); - b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); - b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); + b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data))); + b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded); + b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, + Assert.Collection(results.Entries, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Collection(actual.Value.Data, item => { @@ -87,7 +89,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { Assert.Equal("DegradedCheck", actual.Key); Assert.Equal(DegradedMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status); + Assert.Equal(HealthStatus.Degraded, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Empty(actual.Value.Data); }, @@ -95,7 +97,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { Assert.Equal("UnhealthyCheck", actual.Key); Assert.Equal(UnhealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); Assert.Same(exception, actual.Value.Exception); Assert.Empty(actual.Value.Data); }); @@ -119,21 +121,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); - b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); - b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); + b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data))); + b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded); + b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck"); // Assert - Assert.Collection(results.Results, + Assert.Collection(results.Entries, actual => { Assert.Equal("HealthyCheck", actual.Key); Assert.Equal(HealthyMessage, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); Assert.Collection(actual.Value.Data, item => { @@ -144,7 +146,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks } [Fact] - public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckerToFailedResultAsync() + public async Task CheckHealthAsync_SetsRegistrationForEachCheck() { // Arrange var thrownException = new InvalidOperationException("Whoops!"); @@ -152,35 +154,79 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddCheck("Throws", ct => throw thrownException); - b.AddCheck("Faults", ct => Task.FromException(faultedException)); - b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())); + b.AddCheck("A"); + b.AddCheck("B"); + b.AddCheck("C"); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, + Assert.Collection( + results.Entries, + actual => + { + Assert.Equal("A", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "A"))); + }, + actual => + { + Assert.Equal("B", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "B"))); + }, + actual => + { + Assert.Equal("C", actual.Key); + Assert.Collection( + actual.Value.Data, + kvp => Assert.Equal(kvp, new KeyValuePair("name", "C"))); + }); + } + + [Fact] + public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToFailedResultAsync() + { + // Arrange + var thrownException = new InvalidOperationException("Whoops!"); + var faultedException = new InvalidOperationException("Ohnoes!"); + + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("Throws", ct => throw thrownException); + b.AddAsyncCheck("Faults", ct => Task.FromException(faultedException)); + b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Passed())); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Entries, actual => { Assert.Equal("Throws", actual.Key); Assert.Equal(thrownException.Message, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status); + Assert.Equal(HealthStatus.Failed, actual.Value.Status); Assert.Same(thrownException, actual.Value.Exception); }, actual => { Assert.Equal("Faults", actual.Key); Assert.Equal(faultedException.Message, actual.Value.Description); - Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status); + Assert.Equal(HealthStatus.Failed, actual.Value.Status); Assert.Same(faultedException, actual.Value.Exception); }, actual => { Assert.Equal("Succeeds", actual.Key); - Assert.Empty(actual.Value.Description); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Null(actual.Value.Description); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); Assert.Null(actual.Value.Exception); }); } @@ -190,12 +236,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { // Arrange var sink = new TestSink(); - var check = new HealthCheck("TestScope", cancellationToken => + var check = new DelegateHealthCheck(cancellationToken => { Assert.Collection(sink.Scopes, actual => { - Assert.Equal(actual.LoggerName, typeof(HealthCheckService).FullName); + Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName); Assert.Collection((IEnumerable>)actual.Scope, item => { @@ -203,7 +249,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks Assert.Equal("TestScope", item.Value); }); }); - return Task.FromResult(HealthCheckResult.Healthy()); + return Task.FromResult(HealthCheckResult.Passed()); }); var loggerFactory = new TestLoggerFactory(sink, enabled: true); @@ -212,36 +258,20 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Override the logger factory for testing b.Services.AddSingleton(loggerFactory); - b.AddCheck(check); + b.AddCheck("TestScope", check); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Results, actual => + Assert.Collection(results.Entries, actual => { Assert.Equal("TestScope", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } - [Fact] - public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult() - { - // Arrange - var service = CreateHealthChecksService(b => - { - b.AddCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))); - }); - - // Act - var ex = await Assert.ThrowsAsync(() => service.CheckHealthAsync()); - - // Assert - Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message); - } - [Fact] public async Task CheckHealthAsync_CheckCanDependOnTransientService() { @@ -250,7 +280,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddTransient(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -258,11 +288,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } @@ -274,7 +304,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddScoped(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -282,11 +312,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } @@ -298,7 +328,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.Services.AddSingleton(); - b.AddCheck(); + b.AddCheck("Test"); }); // Act @@ -306,15 +336,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks // Assert Assert.Collection( - results.Results, + results.Entries, actual => { Assert.Equal("Test", actual.Key); - Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status); + Assert.Equal(HealthStatus.Healthy, actual.Value.Status); }); } - private static HealthCheckService CreateHealthChecksService(Action configure) + private static DefaultHealthCheckService CreateHealthChecksService(Action configure) { var services = new ServiceCollection(); services.AddLogging(); @@ -326,7 +356,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks configure(builder); } - return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); + return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService(); } private class AnotherService { } @@ -336,12 +366,22 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public CheckWithServiceDependency(AnotherService _) { } - - public string Name => "Test"; - - public Task CheckHealthAsync(CancellationToken cancellationToken = default) + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - return Task.FromResult(HealthCheckResult.Healthy()); + return Task.FromResult(HealthCheckResult.Passed()); + } + } + + private class NameCapturingCheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var data = new Dictionary() + { + { "name", context.Registration.Name }, + }; + return Task.FromResult(HealthCheckResult.Passed(data: data)); } } } diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs new file mode 100644 index 0000000000..c4974013c7 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs @@ -0,0 +1,257 @@ +// 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. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection +{ + // Integration tests for extension methods on IHealthCheckBuilder + // + // We test the longest overload of each 'family' of Add...Check methods, since they chain to each other. + public class HealthChecksBuilderTest + { + [Fact] + public void AddCheck_Instance() + { + // Arrange + var instance = new DelegateHealthCheck((_) => + { + return Task.FromResult(HealthCheckResult.Passed()); + }); + + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded,tags: new[] { "tag", }, instance: instance); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.Same(instance, registration.Factory(serviceProvider)); + } + + [Fact] + public void AddCheck_T_TypeActivated() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddCheck_T_Service() + { + // Arrange + var instance = new TestHealthCheck(); + + var services = CreateServices(); + services.AddSingleton(instance); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.Same(instance, registration.Factory(serviceProvider)); + } + + [Fact] + public void AddTypeActivatedCheck() + { + // Arrange + var services = CreateServices(); + services + .AddHealthChecks() + .AddTypeActivatedCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, args: new object[] { 5, "hi", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + + var check = Assert.IsType(registration.Factory(serviceProvider)); + Assert.Equal(5, check.I); + Assert.Equal("hi", check.S); + } + + [Fact] + public void AddDelegateCheck_NoArg() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, check: () => + { + return HealthCheckResult.Passed(); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddDelegateCheck_CancellationToken() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddCheck("test", (_) => + { + return HealthCheckResult.Passed(); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddAsyncDelegateCheck_NoArg() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddAsyncCheck("test", () => + { + return Task.FromResult(HealthCheckResult.Passed()); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void AddAsyncDelegateCheck_CancellationToken() + { + // Arrange + var services = CreateServices(); + services.AddHealthChecks().AddAsyncCheck("test", (_) => + { + return Task.FromResult(HealthCheckResult.Passed()); + }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetRequiredService>().Value; + + // Assert + var registration = Assert.Single(options.Registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag", }, registration.Tags); + Assert.IsType(registration.Factory(serviceProvider)); + } + + [Fact] + public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks() + { + var services = new ServiceCollection(); + services + .AddHealthChecks() + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed())); + services + .AddHealthChecks() + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Passed())); + + // Act + var options = services.BuildServiceProvider().GetRequiredService>(); + + // Assert + Assert.Collection( + options.Value.Registrations, + actual => Assert.Equal("Foo", actual.Name), + actual => Assert.Equal("Bar", actual.Name)); + } + + private IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + return services; + } + + private class TestHealthCheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + } + + private class TestHealthCheckWithArgs : IHealthCheck + { + public TestHealthCheckWithArgs(int i, string s) + { + I = i; + S = s; + } + + public int I { get; set; } + + public string S { get; set; } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs similarity index 76% rename from test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs rename to test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs index b33bd36eb6..34b92b7b5e 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/ServiceCollectionExtensionsTests.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs @@ -1,12 +1,12 @@ // 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. -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Xunit; -namespace Microsoft.Extensions.Diagnostics.HealthChecks +namespace Microsoft.Extensions.DependencyInjection { - public class ServiceCollectionExtensionsTests + public class ServiceCollectionExtensionsTest { [Fact] public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently() @@ -23,8 +23,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks actual => { Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); - Assert.Equal(typeof(IHealthCheckService), actual.ServiceType); - Assert.Equal(typeof(HealthCheckService), actual.ImplementationType); + Assert.Equal(typeof(HealthCheckService), actual.ServiceType); + Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType); Assert.Null(actual.ImplementationInstance); Assert.Null(actual.ImplementationFactory); }); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs deleted file mode 100644 index f1bcbbf00b..0000000000 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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. - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Microsoft.Extensions.Diagnostics.HealthChecks -{ - public class HealthChecksBuilderTests - { - [Fact] - public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks() - { - var services = new ServiceCollection(); - services.AddHealthChecks() - .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy())); - services.AddHealthChecks() - .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy())); - - // Act - var checks = services.BuildServiceProvider().GetRequiredService>(); - - // Assert - Assert.Collection( - checks, - actual => Assert.Equal("Foo", actual.Name), - actual => Assert.Equal("Bar", actual.Name)); - } - } -} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs new file mode 100644 index 0000000000..9ce0a4b620 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs @@ -0,0 +1,31 @@ +// 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. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public class HealthReportTest + { + [Theory] + [InlineData(HealthStatus.Healthy)] + [InlineData(HealthStatus.Degraded)] + [InlineData(HealthStatus.Unhealthy)] + [InlineData(HealthStatus.Failed)] + public void Status_MatchesWorstStatusInResults(HealthStatus status) + { + var result = new HealthReport(new Dictionary() + { + {"Foo", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Bar", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Baz", new HealthReportEntry(status, exception: null, description: null, data: null) }, + {"Quick", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Quack", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + {"Quock", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, + }); + + Assert.Equal(status, result.Status); + } + } +} From 8fb6c2a50acbf0118a39aeec6af0df898acc142f Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 19 Sep 2018 21:22:48 -0700 Subject: [PATCH 41/57] Allow cancellation to propagate --- .../DefaultHealthCheckService.cs | 4 ++- .../DefaultHealthCheckServiceTest.cs | 33 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs index b7a34ff35a..eae633dca0 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -77,7 +77,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime()); } - catch (Exception ex) + + // Allow cancellation to propagate. + catch (Exception ex) when (ex as OperationCanceledException == null) { entry = new HealthReportEntry(HealthStatus.Failed, ex.Message, ex, data: null); Log.HealthCheckError(_logger, registration, ex, stopwatch.GetElapsedTime()); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs index 024309a024..6c74ddcdf0 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs @@ -33,7 +33,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))); var services = serviceCollection.BuildServiceProvider(); - + var scopeFactory = services.GetRequiredService(); var options = services.GetRequiredService>(); var logger = services.GetRequiredService>(); @@ -188,6 +188,35 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks }); } + [Fact] + public async Task CheckHealthAsync_Cancellation_CanPropagate() + { + // Arrange + var insideCheck = new TaskCompletionSource(); + + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("cancels", async ct => + { + insideCheck.SetResult(null); + + await Task.Delay(10000, ct); + return HealthCheckResult.Failed(); + }); + }); + + var cancel = new CancellationTokenSource(); + var task = service.CheckHealthAsync(cancel.Token); + + // After this returns we know the check has started + await insideCheck.Task; + + cancel.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync(async () => await task); + } + [Fact] public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToFailedResultAsync() { @@ -366,7 +395,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public CheckWithServiceDependency(AnotherService _) { } - + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { return Task.FromResult(HealthCheckResult.Passed()); From ea980c55f6148386f726279cf91478f0d8749328 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 23 Sep 2018 19:10:46 +0000 Subject: [PATCH 42/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 58 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index e80c82a59e..cc58ccf920 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,35 +3,35 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180911.1 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 - 2.2.0-preview3-35252 + 2.2.0-preview1-20180918.1 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 + 2.2.0-preview3-35301 2.0.9 2.1.3 2.2.0-preview2-26905-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 7124f37441..649bf2ba0b 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180911.1 -commithash:ddfecdfc6e8e4859db5a0daea578070b862aac65 +version:2.2.0-preview1-20180918.1 +commithash:ad5e3fc53442741a0dd49bce437d2ac72f4b5800 From 18145880fa8a8832a40e8e79d53691ba78d823ff Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sat, 22 Sep 2018 19:21:16 -0700 Subject: [PATCH 43/57] Improved logging for health checks - Add logging of description/data - Add logging for aggregate begin/end --- .../HealthChecksSample/GCInfoHealthCheck.cs | 4 +- samples/HealthChecksSample/Program.cs | 2 +- .../DefaultHealthCheckService.cs | 130 ++++++++++++++++-- 3 files changed, 124 insertions(+), 12 deletions(-) diff --git a/samples/HealthChecksSample/GCInfoHealthCheck.cs b/samples/HealthChecksSample/GCInfoHealthCheck.cs index 20bcc1eb11..c708f3ed3c 100644 --- a/samples/HealthChecksSample/GCInfoHealthCheck.cs +++ b/samples/HealthChecksSample/GCInfoHealthCheck.cs @@ -65,8 +65,8 @@ namespace HealthChecksSample { "Gen2Collections", GC.CollectionCount(2) }, }; - // Report failure if the allocated memory is >= the threshold - var result = allocated >= options.Threshold; + // Report failure if the allocated memory is >= the threshold. Negated because true == success + var result = !(allocated >= options.Threshold); return Task.FromResult(new HealthCheckResult( result, diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index 425b0b3f1a..0ed4ff6e3a 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -15,7 +15,7 @@ namespace HealthChecksSample { _scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "", typeof(DBHealthStartup) }, + { "", typeof(CustomWriterStartup) }, { "basic", typeof(BasicStartup) }, { "writer", typeof(CustomWriterStartup) }, { "liveness", typeof(LivenessProbeStartup) }, diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs index eae633dca0..f1a41ea303 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; 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 // 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. - ValidateRegistrations(_options.Value.Registrations); + ValidateRegistrations(_options.Value.Registrations); } public override async Task CheckHealthAsync( Func predicate, @@ -44,6 +46,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var context = new HealthCheckContext(); var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); + var totalTime = ValueStopwatch.StartNew(); + Log.HealthCheckProcessingBegin(_logger); + foreach (var registration in registrations) { if (predicate != null && !predicate(registration)) @@ -63,7 +68,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks context.Registration = registration; Log.HealthCheckBegin(_logger, registration); - + HealthReportEntry entry; try { @@ -76,6 +81,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks result.Data); Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime()); + Log.HealthCheckData(_logger, registration, entry); } // 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 readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin"); - public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd"); - public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError"); + public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin"); + public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd"); + + 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 _healthCheckProcessingBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckProcessingBegin, + "Running health checks"); + + private static readonly Action _healthCheckProcessingEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckProcessingEnd, + "Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}"); + private static readonly Action _healthCheckBegin = LoggerMessage.Define( LogLevel.Debug, EventIds.HealthCheckBegin, "Running health check {HealthCheckName}"); - private static readonly Action _healthCheckEnd = LoggerMessage.Define( + private static readonly Action _healthCheckEnd = LoggerMessage.Define( LogLevel.Debug, 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 _healthCheckError = LoggerMessage.Define( LogLevel.Error, EventIds.HealthCheckError, "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) { _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) { - _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) { _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> + { + private readonly string _name; + private readonly List> _values; + + private string _formatted; + + public HealthCheckDataLogValue(string name, IReadOnlyDictionary 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("HealthCheckName", name)); + } + + public KeyValuePair this[int index] + { + get + { + if (index < 0 || index >= Count) + { + throw new IndexOutOfRangeException(nameof(index)); + } + + return _values[index]; + } + } + + public int Count => _values.Count; + + public IEnumerator> 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; + } } } } From 204ff0a78504b3c688d906bab4e03e87d8ea1b76 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sat, 22 Sep 2018 18:38:38 -0700 Subject: [PATCH 44/57] Set cache headers in health check middleware --- build/dependencies.props | 1 + .../HealthCheckMiddleware.cs | 12 +++- .../HealthCheckOptions.cs | 8 +++ ...AspNetCore.Diagnostics.HealthChecks.csproj | 1 + .../HealthCheckMiddlewareTests.cs | 58 ++++++++++++++++++- 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index cc58ccf920..7a3fa9d10c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -36,6 +36,7 @@ 2.1.3 2.2.0-preview2-26905-02 15.6.1 + 2.2.0-preview3-35252 4.7.49 2.0.3 11.0.2 diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index e06ef8509b..2e1897bc2a 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { @@ -70,6 +71,15 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks 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) { await _healthCheckOptions.ResponseWriter(httpContext, result); @@ -103,7 +113,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks if (notFound.Count > 0) { - var message = + var message = $"The following health checks were not found: '{string.Join(", ", notFound)}'. " + $"Registered health checks: '{string.Join(", ", checks.Keys)}'."; throw new InvalidOperationException(message); diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index 42996e3701..d66a59e124 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -46,5 +46,13 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks /// of as a string. /// public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; + + /// + /// Gets or sets a value that controls whether the health check middleware will add HTTP headers to prevent + /// response caching. If the value is false the health check middleware will set or override the + /// Cache-Control, Expires, and Pragma headers to prevent response caching. If the value + /// is true the health check middleware will not modify the cache headers of the response. + /// + public bool SuppressCacheHeaders { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj index b7dd85d4a6..15bbc62f78 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj @@ -18,6 +18,7 @@ + diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 7a2435948c..4715a36da9 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -1,15 +1,16 @@ // 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. -using System.Net; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Net.Http.Headers; using Newtonsoft.Json; +using System.Net; +using System.Threading.Tasks; using Xunit; namespace Microsoft.AspNetCore.Diagnostics.HealthChecks @@ -291,6 +292,57 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks 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] public async Task CanFilterChecks() { @@ -363,7 +415,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { services.AddHealthChecks(); }); - + var server = new TestServer(builder); var client = server.CreateClient(); From c29b0e61317ea4da61406fbaa75b8bb0a1345db7 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Thu, 27 Sep 2018 15:30:51 -0700 Subject: [PATCH 45/57] Update LICENSE.txt --- LICENSE.txt | 207 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 197 insertions(+), 10 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 7b2956ecee..b3b180cd51 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,14 +1,201 @@ -Copyright (c) .NET Foundation and Contributors + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -All rights reserved. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at + 1. Definitions. - http://www.apache.org/licenses/LICENSE-2.0 + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) .NET Foundation and Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From fa961b003f74f0e6dbeebe97a992a447b043764e Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Date: Fri, 28 Sep 2018 18:14:58 +0200 Subject: [PATCH 46/57] Added execution time duration (HealthReportEntry TotalDuration) (#493) * Added execution time duration into HealthReportEntry and TotalDuration on HealthReport * review PR feedback from @rynowak. * added the same duration into HealthReportEntry and log when the health check throw --- .../DefaultHealthCheckService.cs | 28 ++++++++++++------ .../HealthReport.cs | 10 ++++++- .../HealthReportEntry.cs | 9 +++++- .../HealthReportTest.cs | 29 ++++++++++++++----- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs index f1a41ea303..1b08acb60b 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -73,30 +73,40 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks try { var result = await healthCheck.CheckHealthAsync(context, cancellationToken); + var duration = stopwatch.GetElapsedTime(); entry = new HealthReportEntry( - result.Result ? HealthStatus.Healthy : registration.FailureStatus, - result.Description, - result.Exception, - result.Data); + status: result.Result ? HealthStatus.Healthy : registration.FailureStatus, + description: result.Description, + duration: duration, + exception: result.Exception, + data: result.Data); - Log.HealthCheckEnd(_logger, registration, entry, stopwatch.GetElapsedTime()); + Log.HealthCheckEnd(_logger, registration, entry, duration); Log.HealthCheckData(_logger, registration, entry); } // Allow cancellation to propagate. catch (Exception ex) when (ex as OperationCanceledException == null) { - entry = new HealthReportEntry(HealthStatus.Failed, ex.Message, ex, data: null); - Log.HealthCheckError(_logger, registration, ex, stopwatch.GetElapsedTime()); + var duration = stopwatch.GetElapsedTime(); + entry = new HealthReportEntry( + status: HealthStatus.Failed, + description: ex.Message, + duration: duration, + exception: ex, + data: null); + + Log.HealthCheckError(_logger, registration, ex, duration); } entries[registration.Name] = entry; } } - var report = new HealthReport(entries); - Log.HealthCheckProcessingEnd(_logger, report.Status, totalTime.GetElapsedTime()); + var totalElapsedTime = totalTime.GetElapsedTime(); + var report = new HealthReport(entries, totalElapsedTime); + Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime); return report; } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs index 29359a547f..a0e6fac1cd 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs @@ -1,6 +1,7 @@ // 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. +using System; using System.Collections.Generic; namespace Microsoft.Extensions.Diagnostics.HealthChecks @@ -14,10 +15,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// Create a new from the specified results. /// /// A containing the results from each health check. - public HealthReport(IReadOnlyDictionary entries) + /// A value indicating the time the health check service took to execute. + public HealthReport(IReadOnlyDictionary entries, TimeSpan totalDuration) { Entries = entries; Status = CalculateAggregateStatus(entries.Values); + TotalDuration = totalDuration; } /// @@ -35,6 +38,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// public HealthStatus Status { get; } + /// + /// Gets the time the health check service took to execute. + /// + public TimeSpan TotalDuration { get; } + private HealthStatus CalculateAggregateStatus(IEnumerable entries) { // This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs index 17ed5ae288..2898c93aa4 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs @@ -19,12 +19,14 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// /// A value indicating the health status of the component that was checked. /// A human-readable description of the status of the component that was checked. + /// A value indicating the health execution duration. /// An representing the exception that was thrown when checking for status (if any). /// Additional key-value pairs describing the health of the component. - public HealthReportEntry(HealthStatus status, string description, Exception exception, IReadOnlyDictionary data) + public HealthReportEntry(HealthStatus status, string description, TimeSpan duration, Exception exception, IReadOnlyDictionary data) { Status = status; Description = description; + Duration = duration; Exception = exception; Data = data ?? _emptyReadOnlyDictionary; } @@ -39,6 +41,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// public string Description { get; } + /// + /// Gets the health check execution duration. + /// + public TimeSpan Duration { get; } + /// /// Gets an representing the exception that was thrown when checking for status (if any). /// diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs index 9ce0a4b620..453398e639 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs @@ -1,6 +1,7 @@ // 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. +using System; using System.Collections.Generic; using Xunit; @@ -17,15 +18,29 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { var result = new HealthReport(new Dictionary() { - {"Foo", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, - {"Bar", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, - {"Baz", new HealthReportEntry(status, exception: null, description: null, data: null) }, - {"Quick", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, - {"Quack", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, - {"Quock", new HealthReportEntry(HealthStatus.Healthy, null, null, null) }, - }); + {"Foo", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) }, + {"Bar", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue,null, null) }, + {"Baz", new HealthReportEntry(status, exception: null, description: null,duration:TimeSpan.MinValue, data: null) }, + {"Quick", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) }, + {"Quack", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) }, + {"Quock", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) }, + }, totalDuration: TimeSpan.MinValue); Assert.Equal(status, result.Status); } + + [Theory] + [InlineData(200)] + [InlineData(300)] + [InlineData(400)] + public void TotalDuration_MatchesTotalDurationParameter(int milliseconds) + { + var result = new HealthReport(new Dictionary() + { + {"Foo", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) } + }, totalDuration: TimeSpan.FromMilliseconds(milliseconds)); + + Assert.Equal(TimeSpan.FromMilliseconds(milliseconds), result.TotalDuration); + } } } From 4fc5c7c11b69b6598efd81f6a60aac7fdca8701d Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Fri, 28 Sep 2018 17:10:32 -0700 Subject: [PATCH 47/57] automated: bulk infrastructure updates. Update bootstrapper scripts and remove unnecessary signing properties --- Directory.Build.props | 3 --- run.ps1 | 6 +++--- run.sh | 10 +++++----- .../Diagnostics.FunctionalTests.csproj | 1 - 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6fc59be2d0..d79e6a446b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,9 +14,6 @@ $(MSBuildThisFileDirectory) $(MSBuildThisFileDirectory)build\Key.snk true - Microsoft - MicrosoftNuGet - true true diff --git a/run.ps1 b/run.ps1 index 3b27382468..34604c7175 100644 --- a/run.ps1 +++ b/run.ps1 @@ -52,8 +52,8 @@ in the file are overridden by command line parameters. Example config file: ```json { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev", + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", + "channel": "master", "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" } ``` @@ -192,7 +192,7 @@ if (!$DotNetHome) { else { Join-Path $PSScriptRoot '.dotnet'} } -if (!$Channel) { $Channel = 'dev' } +if (!$Channel) { $Channel = 'master' } if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } # Execute diff --git a/run.sh b/run.sh index 02aac15874..4c1fed5646 100755 --- a/run.sh +++ b/run.sh @@ -220,7 +220,7 @@ if [ -f "$config_file" ]; then config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" else - _error "$config_file contains invalid JSON." + __error "$config_file contains invalid JSON." exit 1 fi elif __machine_has python ; then @@ -228,7 +228,7 @@ if [ -f "$config_file" ]; then config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" else - _error "$config_file contains invalid JSON." + __error "$config_file contains invalid JSON." exit 1 fi elif __machine_has python3 ; then @@ -236,11 +236,11 @@ if [ -f "$config_file" ]; then config_channel="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" config_tools_source="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" else - _error "$config_file contains invalid JSON." + __error "$config_file contains invalid JSON." exit 1 fi else - _error 'Missing required command: jq or python. Could not parse the JSON file.' + __error 'Missing required command: jq or python. Could not parse the JSON file.' exit 1 fi @@ -248,7 +248,7 @@ if [ -f "$config_file" ]; then [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" fi -[ -z "$channel" ] && channel='dev' +[ -z "$channel" ] && channel='master' [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' get_korebuild diff --git a/test/Diagnostics.FunctionalTests/Diagnostics.FunctionalTests.csproj b/test/Diagnostics.FunctionalTests/Diagnostics.FunctionalTests.csproj index 34524fc4a6..019ffa7fda 100644 --- a/test/Diagnostics.FunctionalTests/Diagnostics.FunctionalTests.csproj +++ b/test/Diagnostics.FunctionalTests/Diagnostics.FunctionalTests.csproj @@ -3,7 +3,6 @@ $(StandardTestTfms) false - false Diagnostics.FunctionalTests From 6a8494f938f05057b4ac0fb6ae9c5dea94d11225 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 30 Sep 2018 12:10:59 -0700 Subject: [PATCH 48/57] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 64 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 7a3fa9d10c..93387f8fc4 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,44 +3,44 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-20180918.1 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 - 2.2.0-preview3-35301 + 2.2.0-preview1-20180928.5 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 2.0.9 2.1.3 - 2.2.0-preview2-26905-02 + 2.2.0-preview3-26927-02 + 2.2.0-preview3-35359 15.6.1 - 2.2.0-preview3-35252 4.7.49 2.0.3 11.0.2 - 4.6.0-preview2-26905-02 + 4.6.0-preview3-26927-02 4.5.0 1.6.0 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 649bf2ba0b..26697a21fa 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180918.1 -commithash:ad5e3fc53442741a0dd49bce437d2ac72f4b5800 +version:2.2.0-preview1-20180928.5 +commithash:43faa29f679f47b88689d645b39e6be5e0055d70 From 014e7eb96387641d1bc1441da48ad32f77fe7d10 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 23 Sep 2018 16:29:28 -0700 Subject: [PATCH 49/57] Add EFCore DbContext check --- DiagnosticsPages.sln | 32 ++++++- samples/HealthChecksSample/DBHealthStartup.cs | 4 +- .../DbContextHealthStartup.cs | 85 ++++++++++++++++ .../HealthChecksSample.csproj | 6 +- samples/HealthChecksSample/MyContext.cs | 20 ++++ samples/HealthChecksSample/Program.cs | 5 +- .../DbContextHealthCheck.cs | 56 +++++++++++ .../DbContextHealthCheckOptions.cs | 15 +++ ...meworkCoreHealthChecksBuilderExtensions.cs | 79 +++++++++++++++ ...cs.HealthChecks.EntityFrameworkCore.csproj | 23 +++++ .../Properties/AssemblyInfo.cs | 3 + .../DbContextHealthCheckTest.cs | 96 +++++++++++++++++++ ...rkCoreHealthChecksBuilderExtensionsTest.cs | 47 +++++++++ ...lthChecks.EntityFrameworkCore.Tests.csproj | 20 ++++ .../TestDbContext.cs | 24 +++++ 15 files changed, 508 insertions(+), 7 deletions(-) create mode 100644 samples/HealthChecksSample/DbContextHealthStartup.cs create mode 100644 samples/HealthChecksSample/MyContext.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheckOptions.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensions.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensionsTest.cs create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/TestDbContext.cs diff --git a/DiagnosticsPages.sln b/DiagnosticsPages.sln index 655c4fcb5a..28e6a87e8a 100644 --- a/DiagnosticsPages.sln +++ b/DiagnosticsPages.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26923.0 MinimumVisualStudioVersion = 15.0.26730.03 @@ -56,6 +56,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagno EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.Tests", "test\Microsoft.Extensions.Diagnostics.HealthChecks.Tests\Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj", "{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore", "src\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj", "{61F8B71C-4BDA-431C-9485-743D619ACF9A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests", "test\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj", "{4416D921-0E43-496D-9156-D7EAA9CB8DD3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -344,6 +348,30 @@ Global {3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|x86.ActiveCfg = Release|Any CPU {3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|x86.Build.0 = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|x86.Build.0 = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|Any CPU.Build.0 = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|x86.ActiveCfg = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|x86.Build.0 = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|x86.Build.0 = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|Any CPU.Build.0 = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|x86.ActiveCfg = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -373,6 +401,8 @@ Global {3B4E60F6-E42D-496E-B96F-71A11DABAEE7} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} {E718CE19-23CC-427F-B06E-B866E9A35924} = {2AF90579-B118-4583-AE88-672EFACB5BC4} {3783E8E4-2E96-4987-A83A-0CCCAF4891C1} = {2AF90579-B118-4583-AE88-672EFACB5BC4} + {61F8B71C-4BDA-431C-9485-743D619ACF9A} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D} + {4416D921-0E43-496D-9156-D7EAA9CB8DD3} = {2AF90579-B118-4583-AE88-672EFACB5BC4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D915AA7B-4ADE-4BAC-AF65-1E800D3F3580} diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs index 43dd6abda6..46639d26ea 100644 --- a/samples/HealthChecksSample/DBHealthStartup.cs +++ b/samples/HealthChecksSample/DBHealthStartup.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.DependencyInjection; namespace HealthChecksSample { // Pass in `--scenario db` at the command line to run this sample. - public class DBHealthStartup + public class DbHealthStartup { - public DBHealthStartup(IConfiguration configuration) + public DbHealthStartup(IConfiguration configuration) { Configuration = configuration; } diff --git a/samples/HealthChecksSample/DbContextHealthStartup.cs b/samples/HealthChecksSample/DbContextHealthStartup.cs new file mode 100644 index 0000000000..62167d2ed5 --- /dev/null +++ b/samples/HealthChecksSample/DbContextHealthStartup.cs @@ -0,0 +1,85 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HealthChecksSample +{ + // Pass in `--scenario dbcontext` at the command line to run this sample. + public class DbContextHealthStartup + { + public DbContextHealthStartup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services.AddHealthChecks() + + // Registers a health check for the MyContext type. By default the name of the health check will be the + // name of the DbContext type. There are other options available through AddDbContextCheck to configure + // failure status, tags, and custom test query. + .AddDbContextCheck(); + + // Registers the MyContext type and configures the database provider. + // + // The health check added by AddDbContextCheck will create instances of MyContext from the service provider, + // and so will reuse the configuration provided here. + services.AddDbContext(options => + { + options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]); + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health. + // + // Since this sample doesn't do anything to create the database by default, this will + // return unhealthy by default. + // + // You can to to /createdatabase and /deletedatabase to create and delete the database + // (respectively), and see how it immediately effects the health status. + // + app.UseHealthChecks("/health"); + + app.Map("/createdatabase", b => b.Run(async (context) => + { + await context.Response.WriteAsync("Creating the database...\n"); + await context.Response.Body.FlushAsync(); + + var myContext = context.RequestServices.GetRequiredService(); + await myContext.Database.EnsureCreatedAsync(); + + await context.Response.WriteAsync("Done\n"); + await context.Response.WriteAsync("Go to /health to see the health status\n"); + })); + + app.Map("/deletedatabase", b => b.Run(async (context) => + { + await context.Response.WriteAsync("Deleting the database...\n"); + await context.Response.Body.FlushAsync(); + + var myContext = context.RequestServices.GetRequiredService(); + await myContext.Database.EnsureDeletedAsync(); + + await context.Response.WriteAsync("Done\n"); + await context.Response.WriteAsync("Go to /health to see the health status\n"); + })); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status\n"); + await context.Response.WriteAsync("Go to /createdatabase to create the database\n"); + await context.Response.WriteAsync("Go to /deletedatabase to create the database\n"); + }); + } + } +} diff --git a/samples/HealthChecksSample/HealthChecksSample.csproj b/samples/HealthChecksSample/HealthChecksSample.csproj index b013e687fa..05b9c50176 100644 --- a/samples/HealthChecksSample/HealthChecksSample.csproj +++ b/samples/HealthChecksSample/HealthChecksSample.csproj @@ -2,8 +2,8 @@ - netcoreapp2.0;net461 - netcoreapp2.0 + netcoreapp2.2;net461 + netcoreapp2.2 @@ -12,12 +12,14 @@ + + diff --git a/samples/HealthChecksSample/MyContext.cs b/samples/HealthChecksSample/MyContext.cs new file mode 100644 index 0000000000..1cfbeaab25 --- /dev/null +++ b/samples/HealthChecksSample/MyContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; + +namespace HealthChecksSample +{ + public class MyContext : DbContext + { + public MyContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Blog { get; set; } + } + + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + } +} diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index 0ed4ff6e3a..56e6e6478f 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -15,12 +15,13 @@ namespace HealthChecksSample { _scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "", typeof(CustomWriterStartup) }, + { "", typeof(BasicStartup) }, { "basic", typeof(BasicStartup) }, { "writer", typeof(CustomWriterStartup) }, { "liveness", typeof(LivenessProbeStartup) }, { "port", typeof(ManagementPortStartup) }, - { "db", typeof(DBHealthStartup) }, + { "db", typeof(DbHealthStartup) }, + { "dbcontext", typeof(DbContextHealthStartup) }, }; } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs new file mode 100644 index 0000000000..9d581e89d3 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs @@ -0,0 +1,56 @@ +// 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. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + internal sealed class DbContextHealthCheck : IHealthCheck where TContext : DbContext + { + private static readonly Func> DefaultTestQuery = (dbContext, cancellationToken) => + { + return dbContext.Database.CanConnectAsync(cancellationToken); + }; + + private readonly TContext _dbContext; + private readonly IOptionsMonitor> _options; + + public DbContextHealthCheck(TContext dbContext, IOptionsMonitor> options) + { + if (dbContext == null) + { + throw new ArgumentNullException(nameof(dbContext)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _dbContext = dbContext; + _options = options; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var options = _options.Get(context.Registration.Name); + var testQuery = options.CustomTestQuery ?? DefaultTestQuery; + + if (await testQuery(_dbContext, cancellationToken)) + { + return HealthCheckResult.Passed(); + } + + return HealthCheckResult.Failed(); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheckOptions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheckOptions.cs new file mode 100644 index 0000000000..7fb330c376 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheckOptions.cs @@ -0,0 +1,15 @@ +// 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. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + internal sealed class DbContextHealthCheckOptions where TContext : DbContext + { + public Func> CustomTestQuery { get; set; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensions.cs new file mode 100644 index 0000000000..bf299bc6b0 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensions.cs @@ -0,0 +1,79 @@ +// 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. + + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class EntityFrameworkCoreHealthChecksBuilderExtensions + { + /// + /// Adds a health check for the specified type. + /// + /// The type. + /// The . + /// + /// The health check name. Optional. If null the type name of will be used for the name. + /// + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// + /// A custom test query that will be executed when the health check executes to test the health of the database + /// connection and configurations. + /// + /// The . + /// + /// + /// The health check implementation added by this method will use the dependency injection container + /// to create an instance of . + /// + /// + /// By default the health check implementation will use the method + /// to test connectivity to the database. This method requires that the database provider has correctly implemented the + /// interface. If the database provide has not implemented this interface + /// then the health check will report a failure. + /// + /// + /// Providing a will replace the use of + /// to test database connectivity. An implementation of a test query should handle exceptions that can arise due to connectivity failure, + /// and should return a pass/fail result. The test query should be be designed to complete in a short and predicatable amount of time. + /// + /// + public static IHealthChecksBuilder AddDbContextCheck( + this IHealthChecksBuilder builder, + string name = null, + HealthStatus? failureStatus = default, + IEnumerable tags = default, + Func> customTestQuery = default) + where TContext : DbContext + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + name = typeof(TContext).Name; + } + + if (customTestQuery != null) + { + builder.Services.Configure>(name, options => options.CustomTestQuery = customTestQuery); + } + + return builder.AddCheck>(name, failureStatus, tags); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj new file mode 100644 index 0000000000..6a9c3cf5b9 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj @@ -0,0 +1,23 @@ + + + + + Components for performing health checks using EntityFrameworkCore. + + netstandard2.0 + $(NoWarn);CS1591 + true + diagnostics;healthchecks;entityframeworkcore + Microsoft.Extensions.Diagnostics.HealthChecks + + + + + + + + + + + + diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Properties/AssemblyInfo.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1aa256d83a --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs new file mode 100644 index 0000000000..23b17d4939 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs @@ -0,0 +1,96 @@ +// 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. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public class DbContextHealthCheckTest + { + // Just testing pass here since it would be complicated to simulate a failure. All of that logic lives in EF anyway. + [Fact] + public async Task CheckAsync_DefaultTest_Pass() + { + // Arrange + var services = CreateServices(); + using (var scope = services.GetRequiredService().CreateScope()) + { + var registration = Assert.Single(services.GetRequiredService>().Value.Registrations); + var check = ActivatorUtilities.CreateInstance>(scope.ServiceProvider); + + // Act + var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); + + // Assert + Assert.True(result.Result, "Health check passed"); + } + } + + [Fact] + public async Task CheckAsync_CustomTest_Pass() + { + // Arrange + var services = CreateServices(async (c, ct) => + { + return 0 < await c.Blogs.CountAsync(); + }); + + using (var scope = services.GetRequiredService().CreateScope()) + { + var registration = Assert.Single(services.GetRequiredService>().Value.Registrations); + var check = ActivatorUtilities.CreateInstance>(scope.ServiceProvider); + + // Add a blog so that the custom test passes + var context = scope.ServiceProvider.GetRequiredService(); + context.Add(new Blog()); + await context.SaveChangesAsync(); + + // Act + var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); + + // Assert + Assert.True(result.Result, "Health check passed"); + } + } + + + [Fact] + public async Task CheckAsync_CustomTest_Fail() + { + // Arrange + var services = CreateServices(async (c, ct) => + { + return 0 < await c.Blogs.CountAsync(); + }); + + using (var scope = services.GetRequiredService().CreateScope()) + { + var registration = Assert.Single(services.GetRequiredService>().Value.Registrations); + var check = ActivatorUtilities.CreateInstance>(scope.ServiceProvider); + + // Act + var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); + + // Assert + Assert.False(result.Result, "Health check failed"); + } + } + + private static IServiceProvider CreateServices(Func> testQuery = null) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddDbContext(o => o.UseInMemoryDatabase("Test")); + + var builder = serviceCollection.AddHealthChecks(); + builder.AddDbContextCheck("test", HealthStatus.Degraded, new[] { "tag1", "tag2", }, testQuery); + return serviceCollection.BuildServiceProvider(); + } + } +} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensionsTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensionsTest.cs new file mode 100644 index 0000000000..9efcb025e0 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensionsTest.cs @@ -0,0 +1,47 @@ +// 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. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection +{ + public class EntityFrameworkCoreHealthChecksBuilderExtensionsTest + { + [Fact] + public void AddDbContextCheck_RegistersDbContextHealthCheck() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddDbContext(o => o.UseInMemoryDatabase("Test")); + + var builder = serviceCollection.AddHealthChecks(); + + // Act + builder.AddDbContextCheck("test", HealthStatus.Degraded, new[] { "tag1", "tag2", }, (c, ct) => Task.FromResult(true)); + + // Assert + var services = serviceCollection.BuildServiceProvider(); + + var registrations = services.GetRequiredService>().Value.Registrations; + + var registration = Assert.Single(registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag1", "tag2", }, registration.Tags.ToArray()); + + var options = services.GetRequiredService>>(); + Assert.NotNull(options.Get("test").CustomTestQuery); + + using (var scope = services.GetRequiredService().CreateScope()) + { + var check = Assert.IsType>(registration.Factory(scope.ServiceProvider)); + } + } + } +} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj new file mode 100644 index 0000000000..be5627dca4 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj @@ -0,0 +1,20 @@ + + + + $(StandardTestTfms) + Microsoft.AspNetCore.Diagnostics.HealthChecks + + + + + + + + + + + + + + + diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/TestDbContext.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/TestDbContext.cs new file mode 100644 index 0000000000..e38ed93674 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/TestDbContext.cs @@ -0,0 +1,24 @@ +// 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. + +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Diagnostics.HealthChecks +{ + public class TestDbContext : DbContext + { + public TestDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Blogs { get; set; } + } + + public class Blog + { + public int Id { get; set; } + + public int Name { get; set; } + } +} From 9722d895724f6af65812873b57fdf38378013f57 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 5 Oct 2018 22:28:56 -0700 Subject: [PATCH 50/57] Adjust log levels --- .../HealthStatus.cs | 4 ++ .../DefaultHealthCheckService.cs | 41 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs index d4293cb7b4..ad3f40a201 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs @@ -12,6 +12,10 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// and the corresponding value of . /// /// + /// A status of should be considered the default value for a failing health check. Application + /// developers may configure a health check to report a different status as desired. + /// + /// /// The values of this enum or ordered from least healthy to most healthy. So is /// greater than but less than . /// diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs index 1b08acb60b..0e907d7872 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -154,10 +154,28 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks EventIds.HealthCheckBegin, "Running health check {HealthCheckName}"); - private static readonly Action _healthCheckEnd = LoggerMessage.Define( + // These are separate so they can have different log levels + private static readonly string HealthCheckEndText = "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'"; + + private static readonly Action _healthCheckEndHealthy = LoggerMessage.Define( LogLevel.Debug, EventIds.HealthCheckEnd, - "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'"); + HealthCheckEndText); + + private static readonly Action _healthCheckEndDegraded = LoggerMessage.Define( + LogLevel.Warning, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckEndUnhealthy = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckEnd, + HealthCheckEndText); + + private static readonly Action _healthCheckEndFailed = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckEnd, + HealthCheckEndText); private static readonly Action _healthCheckError = LoggerMessage.Define( LogLevel.Error, @@ -181,7 +199,24 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration) { - _healthCheckEnd(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + switch (entry.Status) + { + case HealthStatus.Healthy: + _healthCheckEndHealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + + case HealthStatus.Degraded: + _healthCheckEndDegraded(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + + case HealthStatus.Unhealthy: + _healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + + case HealthStatus.Failed: + _healthCheckEndFailed(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); + break; + } } public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration) From 1f31e0556d7cfb7fc99c6c8650173d0cc4ba0717 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 8 Oct 2018 17:58:45 -0700 Subject: [PATCH 51/57] Add IHealthCheckPublisher for push-based checks (#498) IHealthCheckPublisher allows you to configure and run health checks regularly inside an application, and push the notifications elsewhere. All publishers are part of a single queue with a configurable period and timeout. --- build/dependencies.props | 2 + samples/WelcomePageSample/web.config | 9 - .../HealthReport.cs | 0 .../HealthReportEntry.cs | 0 .../IHealthCheckPublisher.cs | 39 ++ .../DefaultHealthCheckService.cs | 22 +- .../HealthCheckServiceCollectionExtensions.cs | 4 +- .../HealthCheckPublisherHostedService.cs | 262 +++++++++ .../HealthCheckPublisherOptions.cs | 84 +++ ...Extensions.Diagnostics.HealthChecks.csproj | 4 +- ...Core.Diagnostics.HealthChecks.Tests.csproj | 3 +- .../ServiceCollectionExtensionsTest.cs | 12 +- .../HealthCheckPublisherHostedServiceTest.cs | 528 ++++++++++++++++++ 13 files changed, 944 insertions(+), 25 deletions(-) delete mode 100644 samples/WelcomePageSample/web.config rename src/{Microsoft.Extensions.Diagnostics.HealthChecks => Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions}/HealthReport.cs (100%) rename src/{Microsoft.Extensions.Diagnostics.HealthChecks => Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions}/HealthReportEntry.cs (100%) create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheckPublisher.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckPublisherHostedService.cs create mode 100644 src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckPublisherOptions.cs create mode 100644 test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckPublisherHostedServiceTest.cs diff --git a/build/dependencies.props b/build/dependencies.props index 93387f8fc4..d382aa618d 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -27,6 +27,8 @@ 2.2.0-preview3-35359 2.2.0-preview3-35359 2.2.0-preview3-35359 + 2.2.0-preview3-35359 + 2.2.0-preview3-35359 2.2.0-preview3-35359 2.2.0-preview3-35359 2.2.0-preview3-35359 diff --git a/samples/WelcomePageSample/web.config b/samples/WelcomePageSample/web.config deleted file mode 100644 index f7ac679334..0000000000 --- a/samples/WelcomePageSample/web.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReport.cs similarity index 100% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReport.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReport.cs diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReportEntry.cs similarity index 100% rename from src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthReportEntry.cs rename to src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReportEntry.cs diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheckPublisher.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheckPublisher.cs new file mode 100644 index 0000000000..f1809c4bb8 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/IHealthCheckPublisher.cs @@ -0,0 +1,39 @@ +// 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. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Represents a publisher of information. + /// + /// + /// + /// The default health checks implementation provided an IHostedService implementation that can + /// be used to execute health checks at regular intervals and provide the resulting + /// data to all registered instances. + /// + /// + /// To provide an implementation, register an instance or type as a singleton + /// service in the dependency injection container. + /// + /// + /// instances are provided with a after executing + /// health checks in a background thread. The use of depend on hosting in + /// an application using IWebHost or generic host (IHost). Execution of + /// instance is not related to execution of health checks via a middleware. + /// + /// + public interface IHealthCheckPublisher + { + /// + /// Publishes the provided . + /// + /// The . The result of executing a set of health checks. + /// The . + /// A which will complete when publishing is complete. + Task PublishAsync(HealthReport report, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs index 0e907d7872..aa12d97418 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -126,19 +126,19 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks } } + internal static class EventIds + { + public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin"); + public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd"); + + 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 class Log { - public static class EventIds - { - public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin"); - public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd"); - - 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 _healthCheckProcessingBegin = LoggerMessage.Define( LogLevel.Debug, EventIds.HealthCheckProcessingBegin, diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs index 76b0dc45c1..d6df03d2ae 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthCheckServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; namespace Microsoft.Extensions.DependencyInjection { @@ -24,7 +25,8 @@ namespace Microsoft.Extensions.DependencyInjection /// An instance of from which health checks can be registered. public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAddSingleton(); + services.TryAddSingleton(); return new HealthChecksBuilder(services); } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckPublisherHostedService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckPublisherHostedService.cs new file mode 100644 index 0000000000..d124ffa2e3 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckPublisherHostedService.cs @@ -0,0 +1,262 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + internal sealed class HealthCheckPublisherHostedService : IHostedService + { + private readonly HealthCheckService _healthCheckService; + private readonly IOptions _options; + private readonly ILogger _logger; + private readonly IHealthCheckPublisher[] _publishers; + + private CancellationTokenSource _stopping; + private Timer _timer; + + public HealthCheckPublisherHostedService( + HealthCheckService healthCheckService, + IOptions options, + ILogger logger, + IEnumerable publishers) + { + if (healthCheckService == null) + { + throw new ArgumentNullException(nameof(healthCheckService)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + if (publishers == null) + { + throw new ArgumentNullException(nameof(publishers)); + } + + _healthCheckService = healthCheckService; + _options = options; + _logger = logger; + _publishers = publishers.ToArray(); + + _stopping = new CancellationTokenSource(); + } + + internal bool IsStopping => _stopping.IsCancellationRequested; + + internal bool IsTimerRunning => _timer != null; + + public Task StartAsync(CancellationToken cancellationToken = default) + { + if (_publishers.Length == 0) + { + return Task.CompletedTask; + } + + // IMPORTANT - make sure this is the last thing that happens in this method. The timer can + // fire before other code runs. + _timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken = default) + { + try + { + _stopping.Cancel(); + } + catch + { + // Ignore exceptions thrown as a result of a cancellation. + } + + if (_publishers.Length == 0) + { + return Task.CompletedTask; + } + + _timer?.Dispose(); + _timer = null; + + + return Task.CompletedTask; + } + + // Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync + private async void Timer_Tick(object state) + { + await RunAsync(); + } + + // Internal for testing + internal async Task RunAsync() + { + var duration = ValueStopwatch.StartNew(); + Logger.HealthCheckPublisherProcessingBegin(_logger); + + CancellationTokenSource cancellation = null; + try + { + var timeout = _options.Value.Timeout; + + cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token); + cancellation.CancelAfter(timeout); + + await RunAsyncCore(cancellation.Token); + + Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime()); + } + catch (OperationCanceledException) when (IsStopping) + { + // This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's + // a timeout and we want to log it. + } + catch (Exception ex) + { + // This is an error, publishing failed. + Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime(), ex); + } + finally + { + cancellation.Dispose(); + } + } + + private async Task RunAsyncCore(CancellationToken cancellationToken) + { + // Forcibly yield - we want to unblock the timer thread. + await Task.Yield(); + + // The health checks service does it's own logging, and doesn't throw exceptions. + var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken); + + var publishers = _publishers; + var tasks = new Task[publishers.Length]; + for (var i = 0; i < publishers.Length; i++) + { + tasks[i] = RunPublisherAsync(publishers[i], report, cancellationToken); + } + + await Task.WhenAll(tasks); + } + + private async Task RunPublisherAsync(IHealthCheckPublisher publisher, HealthReport report, CancellationToken cancellationToken) + { + var duration = ValueStopwatch.StartNew(); + + try + { + Logger.HealthCheckPublisherBegin(_logger, publisher); + + await publisher.PublishAsync(report, cancellationToken); + Logger.HealthCheckPublisherEnd(_logger, publisher, duration.GetElapsedTime()); + } + catch (OperationCanceledException) when (IsStopping) + { + // This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's + // a timeout and we want to log it. + } + catch (OperationCanceledException ocex) + { + Logger.HealthCheckPublisherTimeout(_logger, publisher, duration.GetElapsedTime()); + throw ocex; + } + catch (Exception ex) + { + Logger.HealthCheckPublisherError(_logger, publisher, duration.GetElapsedTime(), ex); + throw ex; + } + } + + internal static class EventIds + { + public static readonly EventId HealthCheckPublisherProcessingBegin = new EventId(100, "HealthCheckPublisherProcessingBegin"); + public static readonly EventId HealthCheckPublisherProcessingEnd = new EventId(101, "HealthCheckPublisherProcessingEnd"); + public static readonly EventId HealthCheckPublisherProcessingError = new EventId(101, "HealthCheckPublisherProcessingError"); + + public static readonly EventId HealthCheckPublisherBegin = new EventId(102, "HealthCheckPublisherBegin"); + public static readonly EventId HealthCheckPublisherEnd = new EventId(103, "HealthCheckPublisherEnd"); + public static readonly EventId HealthCheckPublisherError = new EventId(104, "HealthCheckPublisherError"); + public static readonly EventId HealthCheckPublisherTimeout = new EventId(104, "HealthCheckPublisherTimeout"); + } + + private static class Logger + { + private static readonly Action _healthCheckPublisherProcessingBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckPublisherProcessingBegin, + "Running health check publishers"); + + private static readonly Action _healthCheckPublisherProcessingEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckPublisherProcessingEnd, + "Health check publisher processing completed after {ElapsedMilliseconds}ms"); + + private static readonly Action _healthCheckPublisherBegin = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckPublisherBegin, + "Running health check publisher '{HealthCheckPublisher}'"); + + private static readonly Action _healthCheckPublisherEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HealthCheckPublisherEnd, + "Health check '{HealthCheckPublisher}' completed after {ElapsedMilliseconds}ms"); + + private static readonly Action _healthCheckPublisherError = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckPublisherError, + "Health check {HealthCheckPublisher} threw an unhandled exception after {ElapsedMilliseconds}ms"); + + private static readonly Action _healthCheckPublisherTimeout = LoggerMessage.Define( + LogLevel.Error, + EventIds.HealthCheckPublisherTimeout, + "Health check {HealthCheckPublisher} was canceled after {ElapsedMilliseconds}ms"); + + public static void HealthCheckPublisherProcessingBegin(ILogger logger) + { + _healthCheckPublisherProcessingBegin(logger, null); + } + + public static void HealthCheckPublisherProcessingEnd(ILogger logger, TimeSpan duration, Exception exception = null) + { + _healthCheckPublisherProcessingEnd(logger, duration.TotalMilliseconds, exception); + } + + public static void HealthCheckPublisherBegin(ILogger logger, IHealthCheckPublisher publisher) + { + _healthCheckPublisherBegin(logger, publisher, null); + } + + public static void HealthCheckPublisherEnd(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration) + { + _healthCheckPublisherEnd(logger, publisher, duration.TotalMilliseconds, null); + } + + public static void HealthCheckPublisherError(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration, Exception exception) + { + _healthCheckPublisherError(logger, publisher, duration.TotalMilliseconds, exception); + } + + public static void HealthCheckPublisherTimeout(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration) + { + _healthCheckPublisherTimeout(logger, publisher, duration.TotalMilliseconds, null); + } + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckPublisherOptions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckPublisherOptions.cs new file mode 100644 index 0000000000..1313718af8 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckPublisherOptions.cs @@ -0,0 +1,84 @@ +// 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. + +using System; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + /// + /// Options for the default service that executes instances. + /// + public sealed class HealthCheckPublisherOptions + { + private TimeSpan _delay; + private TimeSpan _period; + + public HealthCheckPublisherOptions() + { + _delay = TimeSpan.FromSeconds(5); + _period = TimeSpan.FromSeconds(30); + } + + /// + /// Gets or sets the initial delay applied after the application starts before executing + /// instances. The delay is applied once at startup, and does + /// not apply to subsequent iterations. The default value is 5 seconds. + /// + public TimeSpan Delay + { + get => _delay; + set + { + if (value == System.Threading.Timeout.InfiniteTimeSpan) + { + throw new ArgumentException($"The {nameof(Delay)} must not be infinite.", nameof(value)); + } + + _delay = value; + } + } + + /// + /// Gets or sets the period of execution. The default value is + /// 30 seconds. + /// + /// + /// The cannot be set to a value lower than 1 second. + /// + public TimeSpan Period + { + get => _period; + set + { + if (value < TimeSpan.FromSeconds(1)) + { + throw new ArgumentException($"The {nameof(Period)} must be greater than or equal to one second.", nameof(value)); + } + + if (value == System.Threading.Timeout.InfiniteTimeSpan) + { + throw new ArgumentException($"The {nameof(Period)} must not be infinite.", nameof(value)); + } + + _delay = value; + } + } + + /// + /// Gets or sets a predicate that is used to filter the set of health checks executed. + /// + /// + /// If is null, the health check publisher service will run all + /// registered health checks - this is the default behavior. To run a subset of health checks, + /// provide a function that filters the set of checks. The predicate will be evaluated each period. + /// + public Func Predicate { get; set; } + + /// + /// Gets or sets the timeout for executing the health checks an all + /// instances. Use to execute with no timeout. + /// The default value is 30 seconds. + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index 725226023e..523803907a 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -12,8 +12,8 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder diagnostics;healthchecks - - + + diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj index 8ac9320ae8..8f7cd5e90e 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj @@ -1,4 +1,4 @@ - + $(StandardTestTfms) @@ -9,6 +9,7 @@ + diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs index 34b92b7b5e..694a97628d 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/ServiceCollectionExtensionsTest.cs @@ -1,7 +1,9 @@ // 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. +using System.Linq; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Extensions.DependencyInjection @@ -19,7 +21,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddHealthChecks(); // Assert - Assert.Collection(services, + Assert.Collection(services.OrderBy(s => s.ServiceType.FullName), actual => { Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); @@ -27,6 +29,14 @@ namespace Microsoft.Extensions.DependencyInjection Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType); Assert.Null(actual.ImplementationInstance); Assert.Null(actual.ImplementationFactory); + }, + actual => + { + Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); + Assert.Equal(typeof(IHostedService), actual.ServiceType); + Assert.Equal(typeof(HealthCheckPublisherHostedService), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); }); } } diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckPublisherHostedServiceTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckPublisherHostedServiceTest.cs new file mode 100644 index 0000000000..49d57916eb --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckPublisherHostedServiceTest.cs @@ -0,0 +1,528 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public class HealthCheckPublisherHostedServiceTest + { + [Fact] + public async Task StartAsync_WithoutPublishers_DoesNotStartTimer() + { + // Arrange + var publishers = new IHealthCheckPublisher[] + { + }; + + var service = CreateService(publishers); + + try + { + // Act + await service.StartAsync(); + + // Assert + Assert.False(service.IsTimerRunning); + Assert.False(service.IsStopping); + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + } + + [Fact] + public async Task StartAsync_WithPublishers_StartsTimer() + { + // Arrange + var publishers = new IHealthCheckPublisher[] + { + new TestPublisher(), + }; + + var service = CreateService(publishers); + + try + { + // Act + await service.StartAsync(); + + // Assert + Assert.True(service.IsTimerRunning); + Assert.False(service.IsStopping); + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + } + + [Fact] + public async Task StartAsync_WithPublishers_StartsTimer_RunsPublishers() + { + // Arrange + var unblock0 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var unblock1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var unblock2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var publishers = new TestPublisher[] + { + new TestPublisher() { Wait = unblock0.Task, }, + new TestPublisher() { Wait = unblock1.Task, }, + new TestPublisher() { Wait = unblock2.Task, }, + }; + + var service = CreateService(publishers, configure: (options) => + { + options.Delay = TimeSpan.FromMilliseconds(0); + }); + + try + { + // Act + await service.StartAsync(); + + await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + + unblock0.SetResult(null); + unblock1.SetResult(null); + unblock2.SetResult(null); + + // Assert + Assert.True(service.IsTimerRunning); + Assert.False(service.IsStopping); + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + } + + [Fact] + public async Task StopAsync_CancelsExecution() + { + // Arrange + var unblock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var publishers = new TestPublisher[] + { + new TestPublisher() { Wait = unblock.Task, } + }; + + var service = CreateService(publishers); + + try + { + await service.StartAsync(); + + // Start execution + var running = service.RunAsync(); + + // Wait for the publisher to see the cancellation token + await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + Assert.Single(publishers[0].Entries); + + // Act + await service.StopAsync(); // Trigger cancellation + + // Assert + await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + + unblock.SetResult(null); + + await running.TimeoutAfter(TimeSpan.FromSeconds(10)); + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + } + + [Fact] + public async Task RunAsync_WaitsForCompletion_Single() + { + // Arrange + var sink = new TestSink(); + + var unblock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var publishers = new TestPublisher[] + { + new TestPublisher() { Wait = unblock.Task, }, + }; + + var service = CreateService(publishers, sink: sink); + + try + { + await service.StartAsync(); + + // Act + var running = service.RunAsync(); + + await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + + unblock.SetResult(null); + + await running.TimeoutAfter(TimeSpan.FromSeconds(10)); + + // Assert + Assert.True(service.IsTimerRunning); + Assert.False(service.IsStopping); + + for (var i = 0; i < publishers.Length; i++) + { + var report = Assert.Single(publishers[i].Entries).report; + Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k)); + } + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + + Assert.Collection( + sink.Writes, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherEnd, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); }); + } + + // Not testing logs here to avoid differences in logging order + [Fact] + public async Task RunAsync_WaitsForCompletion_Multiple() + { + // Arrange + var unblock0 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var unblock1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var unblock2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var publishers = new TestPublisher[] + { + new TestPublisher() { Wait = unblock0.Task, }, + new TestPublisher() { Wait = unblock1.Task, }, + new TestPublisher() { Wait = unblock2.Task, }, + }; + + var service = CreateService(publishers); + + try + { + await service.StartAsync(); + + // Act + var running = service.RunAsync(); + + await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + + unblock0.SetResult(null); + unblock1.SetResult(null); + unblock2.SetResult(null); + + await running.TimeoutAfter(TimeSpan.FromSeconds(10)); + + // Assert + Assert.True(service.IsTimerRunning); + Assert.False(service.IsStopping); + + for (var i = 0; i < publishers.Length; i++) + { + var report = Assert.Single(publishers[i].Entries).report; + Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k)); + } + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + } + + [Fact] + public async Task RunAsync_PublishersCanTimeout() + { + // Arrange + var sink = new TestSink(); + var unblock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var publishers = new TestPublisher[] + { + new TestPublisher() { Wait = unblock.Task, }, + }; + + var service = CreateService(publishers, sink: sink, configure: (options) => + { + options.Timeout = TimeSpan.FromMilliseconds(50); + }); + + try + { + await service.StartAsync(); + + // Act + var running = service.RunAsync(); + + await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + + await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken); + + unblock.SetResult(null); + + await running.TimeoutAfter(TimeSpan.FromSeconds(10)); + + // Assert + Assert.True(service.IsTimerRunning); + Assert.False(service.IsStopping); + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + + Assert.Collection( + sink.Writes, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherTimeout, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); }); + } + + [Fact] + public async Task RunAsync_CanFilterHealthChecks() + { + // Arrange + var publishers = new TestPublisher[] + { + new TestPublisher(), + new TestPublisher(), + }; + + var service = CreateService(publishers, configure: (options) => + { + options.Predicate = (r) => r.Name == "one"; + }); + + try + { + await service.StartAsync(); + + // Act + await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10)); + + // Assert + for (var i = 0; i < publishers.Length; i++) + { + var report = Assert.Single(publishers[i].Entries).report; + Assert.Equal(new[] { "one", }, report.Entries.Keys.OrderBy(k => k)); + } + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + } + + [Fact] + public async Task RunAsync_HandlesExceptions() + { + // Arrange + var sink = new TestSink(); + var publishers = new TestPublisher[] + { + new TestPublisher() { Exception = new InvalidTimeZoneException(), }, + }; + + var service = CreateService(publishers, sink: sink); + + try + { + await service.StartAsync(); + + // Act + await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10)); + + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + + Assert.Collection( + sink.Writes, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, + entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherError, entry.EventId); }, + entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); }); + } + + // Not testing logging here to avoid flaky ordering issues + [Fact] + public async Task RunAsync_HandlesExceptions_Multiple() + { + // Arrange + var sink = new TestSink(); + var publishers = new TestPublisher[] + { + new TestPublisher() { Exception = new InvalidTimeZoneException(), }, + new TestPublisher(), + new TestPublisher() { Exception = new InvalidTimeZoneException(), }, + }; + + var service = CreateService(publishers, sink: sink); + + try + { + await service.StartAsync(); + + // Act + await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10)); + + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + } + + private HealthCheckPublisherHostedService CreateService( + IHealthCheckPublisher[] publishers, + Action configure = null, + TestSink sink = null) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddOptions(); + serviceCollection.AddLogging(); + serviceCollection.AddHealthChecks() + .AddCheck("one", () => { return HealthCheckResult.Passed(); }) + .AddCheck("two", () => { return HealthCheckResult.Passed(); }); + + // Choosing big values for tests to make sure that we're not dependent on the defaults. + // All of the tests that rely on the timer will set their own values for speed. + serviceCollection.Configure(options => + { + options.Delay = TimeSpan.FromMinutes(5); + options.Period = TimeSpan.FromMinutes(5); + options.Timeout = TimeSpan.FromMinutes(5); + }); + + if (publishers != null) + { + for (var i = 0; i < publishers.Length; i++) + { + serviceCollection.AddSingleton(publishers[i]); + } + } + + if (configure != null) + { + serviceCollection.Configure(configure); + } + + if (sink != null) + { + serviceCollection.AddSingleton(new TestLoggerFactory(sink, enabled: true)); + } + + var services = serviceCollection.BuildServiceProvider(); + return services.GetServices().OfType< HealthCheckPublisherHostedService>().Single(); + } + + private static async Task AssertCancelledAsync(CancellationToken cancellationToken) + { + await Assert.ThrowsAsync(() => Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)); + } + + private class TestPublisher : IHealthCheckPublisher + { + private TaskCompletionSource _started; + + public TestPublisher() + { + _started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public List<(HealthReport report, CancellationToken cancellationToken)> Entries { get; } = new List<(HealthReport report, CancellationToken cancellationToken)>(); + + public Exception Exception { get; set; } + + public Task Started => _started.Task; + + public Task Wait { get; set; } + + public async Task PublishAsync(HealthReport report, CancellationToken cancellationToken) + { + Entries.Add((report, cancellationToken)); + + // Signal that we've started + _started.SetResult(null); + + if (Wait != null) + { + await Wait; + } + + if (Exception != null) + { + throw Exception; + } + + cancellationToken.ThrowIfCancellationRequested(); + } + } + } +} From 1ff9a3d80c2f1f6055e2353cc478d4a4f3ba7a12 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Tue, 16 Oct 2018 14:04:19 +0200 Subject: [PATCH 52/57] Fixed broken link (#504) The link (https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) was broken due to merging of repo's. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64ff041d5c..ac2d28d2f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ Contributing ====== -Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. +Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/AspNetCore/blob/master/CONTRIBUTING.md) in the AspNetCore repo. From f0994e5972a996507adfa66073887137b2511751 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Tue, 16 Oct 2018 12:48:13 -0700 Subject: [PATCH 53/57] Update package branding for 2.2 RTM --- version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.props b/version.props index 761c13440a..690d61d7e3 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ 2.2.0 - preview3 + rtm $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 @@ -10,7 +10,7 @@ $(VersionSuffix)-$(BuildNumber) 0.5.0 - preview3 + rtm $(ExperimentalVersionPrefix) $(ExperimentalVersionPrefix)-$(ExperimentalVersionSuffix)-final $(ExperimentalVersionSuffix)-$(BuildNumber) From b3db95eb2d099371a9d2fc1e4f8ee77b2a660eaf Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Sat, 20 Oct 2018 11:24:50 -0500 Subject: [PATCH 54/57] Change 'create' to 'delete' in message --- samples/HealthChecksSample/DbContextHealthStartup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/HealthChecksSample/DbContextHealthStartup.cs b/samples/HealthChecksSample/DbContextHealthStartup.cs index 62167d2ed5..5f04ea0ea0 100644 --- a/samples/HealthChecksSample/DbContextHealthStartup.cs +++ b/samples/HealthChecksSample/DbContextHealthStartup.cs @@ -78,7 +78,7 @@ namespace HealthChecksSample { await context.Response.WriteAsync("Go to /health to see the health status\n"); await context.Response.WriteAsync("Go to /createdatabase to create the database\n"); - await context.Response.WriteAsync("Go to /deletedatabase to create the database\n"); + await context.Response.WriteAsync("Go to /deletedatabase to delete the database\n"); }); } } From 1afd5b259454f13803f8b536194aff59c8a261ba Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sat, 27 Oct 2018 17:20:38 -0700 Subject: [PATCH 55/57] Don't use Map Fixes aspnet/Diagnostics#511 and aspnet/Diagnostics#514 It's really confusing to people that we use Map. Users expect that the URL they provide for the health check middleware will only process exact matches. The way it behaves when using map is not optimal for some of the common patterns. --- ...HealthCheckApplicationBuilderExtensions.cs | 77 +++++-- .../HealthCheckMiddlewareTests.cs | 197 ++++++++++++++++++ 2 files changed, 253 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs index 6463889f43..6f54f51765 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/Builder/HealthCheckApplicationBuilderExtensions.cs @@ -21,8 +21,10 @@ namespace Microsoft.AspNetCore.Builder /// A reference to the after the operation has completed. /// /// - /// This method will use to - /// listen to health checks requests on the specified URL path. + /// If is set to null or the empty string then the health check middleware + /// will ignore the URL path and process all requests. If is set to a non-empty + /// value, the health check middleware will process requests with a URL that matches the provided value + /// of case-insensitively, allowing for an extra trailing slash ('/') character. /// /// /// The health check middleware will use default settings from . @@ -48,8 +50,10 @@ namespace Microsoft.AspNetCore.Builder /// A reference to the after the operation has completed. /// /// - /// This method will use to - /// listen to health checks requests on the specified URL path. + /// If is set to null or the empty string then the health check middleware + /// will ignore the URL path and process all requests. If is set to a non-empty + /// value, the health check middleware will process requests with a URL that matches the provided value + /// of case-insensitively, allowing for an extra trailing slash ('/') character. /// /// public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, HealthCheckOptions options) @@ -77,8 +81,11 @@ namespace Microsoft.AspNetCore.Builder /// A reference to the after the operation has completed. /// /// - /// This method will use to - /// listen to health checks requests on the specified URL path and port. + /// If is set to null or the empty string then the health check middleware + /// will ignore the URL path and process all requests on the specified port. If is + /// set to a non-empty value, the health check middleware will process requests with a URL that matches the + /// provided value of case-insensitively, allowing for an extra trailing slash ('/') + /// character. /// /// /// The health check middleware will use default settings from . @@ -104,8 +111,11 @@ namespace Microsoft.AspNetCore.Builder /// A reference to the after the operation has completed. /// /// - /// This method will use to - /// listen to health checks requests on the specified URL path and port. + /// If is set to null or the empty string then the health check middleware + /// will ignore the URL path and process all requests on the specified port. If is + /// set to a non-empty value, the health check middleware will process requests with a URL that matches the + /// provided value of case-insensitively, allowing for an extra trailing slash ('/') + /// character. /// /// /// The health check middleware will use default settings from . @@ -142,8 +152,11 @@ namespace Microsoft.AspNetCore.Builder /// A reference to the after the operation has completed. /// /// - /// This method will use to - /// listen to health checks requests on the specified URL path. + /// If is set to null or the empty string then the health check middleware + /// will ignore the URL path and process all requests on the specified port. If is + /// set to a non-empty value, the health check middleware will process requests with a URL that matches the + /// provided value of case-insensitively, allowing for an extra trailing slash ('/') + /// character. /// /// public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, int port, HealthCheckOptions options) @@ -172,8 +185,11 @@ namespace Microsoft.AspNetCore.Builder /// A reference to the after the operation has completed. /// /// - /// This method will use to - /// listen to health checks requests on the specified URL path. + /// If is set to null or the empty string then the health check middleware + /// will ignore the URL path and process all requests on the specified port. If is + /// set to a non-empty value, the health check middleware will process requests with a URL that matches the + /// provided value of case-insensitively, allowing for an extra trailing slash ('/') + /// character. /// /// public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path, string port, HealthCheckOptions options) @@ -204,16 +220,35 @@ namespace Microsoft.AspNetCore.Builder private static void UseHealthChecksCore(IApplicationBuilder app, PathString path, int? port, object[] args) { - if (port == null) + // NOTE: we explicitly don't use Map here because it's really common for multiple health + // check middleware to overlap in paths. Ex: `/health`, `/health/detailed` - this is order + // sensititive with Map, and it's really surprising to people. + // + // See: + // https://github.com/aspnet/Diagnostics/issues/511 + // https://github.com/aspnet/Diagnostics/issues/512 + // https://github.com/aspnet/Diagnostics/issues/514 + + Func predicate = c => { - app.Map(path, b => b.UseMiddleware(args)); - } - else - { - app.MapWhen( - c => c.Connection.LocalPort == port, - b0 => b0.Map(path, b1 => b1.UseMiddleware(args))); - } + return + + // Process the port if we have one + (port == null || c.Connection.LocalPort == port) && + + // We allow you to listen on all URLs by providing the empty PathString. + (!path.HasValue || + + // If you do provide a PathString, want to handle all of the special cases that + // StartsWithSegments handles, but we also want it to have exact match semantics. + // + // Ex: /Foo/ == /Foo (true) + // Ex: /Foo/Bar == /Foo (false) + (c.Request.Path.StartsWithSegments(path, out var remaining) && + string.IsNullOrEmpty(remaining))); + }; + + app.MapWhen(predicate, b => b.UseMiddleware(args)); } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 4715a36da9..7dab1cc262 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -395,6 +395,131 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } + [Fact] + public async Task CanListenWithPath_AcceptsRequestWithExtraSlash() + { + 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("http://localhost:5001/health/"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task CanListenWithPath_AcceptsRequestWithCaseInsensitiveMatch() + { + 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("http://localhost:5001/HEALTH"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CanListenWithPath_RejectsRequestWithExtraSegments() + { + 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("http://localhost:5001/health/detailed"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + // See: https://github.com/aspnet/Diagnostics/issues/511 + [Fact] + public async Task CanListenWithPath_MultipleMiddleware_LeastSpecificFirst() + { + var builder = new WebHostBuilder() + .Configure(app => + { + // Throws if used + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = (c, r) => throw null, + }); + + app.UseHealthChecks("/health/detailed"); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5001/health/detailed"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + // See: https://github.com/aspnet/Diagnostics/issues/511 + [Fact] + public async Task CanListenWithPath_MultipleMiddleware_MostSpecificFirst() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health/detailed"); + + // Throws if used + app.UseHealthChecks("/health", new HealthCheckOptions() + { + ResponseWriter = (c, r) => throw null, + }); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5001/health/detailed"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + [Fact] public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort() { @@ -486,6 +611,78 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + [Fact] + public async Task CanListenOnPort_MultipleMiddleware() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(next => async (context) => + { + // Need to fake setting the connection info. TestServer doesn't + // do that, because it doesn't have a connection. + context.Connection.LocalPort = context.Request.Host.Port.Value; + await next(context); + }); + // Throws if used + app.UseHealthChecks("/health", port: 5001, new HealthCheckOptions() + { + ResponseWriter = (c, r) => throw null, + }); + + app.UseHealthChecks("/health/detailed", port: 5001); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5001/health/detailed"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CanListenOnPort_MultipleMiddleware_DifferentPorts() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(next => async (context) => + { + // Need to fake setting the connection info. TestServer doesn't + // do that, because it doesn't have a connection. + context.Connection.LocalPort = context.Request.Host.Port.Value; + await next(context); + }); + + // Throws if used + app.UseHealthChecks("/health", port: 5002, new HealthCheckOptions() + { + ResponseWriter = (c, r) => throw null, + }); + + app.UseHealthChecks("/health", port: 5001); + }) + .ConfigureServices(services => + { + services.AddHealthChecks(); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var response = await client.GetAsync("http://localhost:5001/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } } } From 4c94bc272b97ee1ff402078c76655801144eb707 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sat, 27 Oct 2018 18:16:02 -0700 Subject: [PATCH 56/57] Rename anti-caching option - Renamed the property for configuration response caching headers - Renamed the options class to avoid conflicts with other type names Fixes https://github.com/aspnet/Diagnostics/issues/509 --- .../HealthCheckMiddleware.cs | 2 +- .../HealthCheckOptions.cs | 16 ++++++++++++---- .../HealthCheckMiddlewareTests.cs | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs index 2e1897bc2a..ba6b870bdc 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks httpContext.Response.StatusCode = statusCode; - if (!_healthCheckOptions.SuppressCacheHeaders) + if (!_healthCheckOptions.AllowCachingResponses) { // Similar to: https://github.com/aspnet/Security/blob/7b6c9cf0eeb149f2142dedd55a17430e7831ea99/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs#L377-L379 var headers = httpContext.Response.Headers; diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index d66a59e124..bf43676a5c 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -48,11 +48,19 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks public Func ResponseWriter { get; set; } = HealthCheckResponseWriters.WriteMinimalPlaintext; /// - /// Gets or sets a value that controls whether the health check middleware will add HTTP headers to prevent - /// response caching. If the value is false the health check middleware will set or override the + /// Gets or sets a value that controls whether responses from the health check middleware can be cached. + /// + /// + /// + /// The health check middleware does not perform caching of any kind. This setting configures whether + /// the middleware will apply headers to the HTTP response that instruct clients to avoid caching. + /// + /// + /// If the value is false the health check middleware will set or override the /// Cache-Control, Expires, and Pragma headers to prevent response caching. If the value /// is true the health check middleware will not modify the cache headers of the response. - /// - public bool SuppressCacheHeaders { get; set; } + /// + /// + public bool AllowCachingResponses { get; set; } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index 7dab1cc262..a3036f9b70 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -324,7 +324,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { app.UseHealthChecks("/health", new HealthCheckOptions() { - SuppressCacheHeaders = true, + AllowCachingResponses = true, }); }) .ConfigureServices(services => From c802d5ef5fba1ba8dfbcb8c3741af2ba15e9d1aa Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 31 Oct 2018 12:51:14 -0700 Subject: [PATCH 57/57] Redesign HealthStatus (again) (#520) * Redesign HealthStatus (again) This change brings back the ability to return Healthy/Degraded/Unhealthy in a HealthCheckResult. We tried making this pass/fail in 2.2.0-preview3 and folks writing health checks for their own use pointed out (rightly so) that it was too limited. It's still possible for the app developer to configure the failure status of a health check, but it requires the health check author to cooperate. I also got rid of HealthStatus.Failed since it raises more questions than it answers. It's really not clear that it's valuable for a health check for behave different when throwing an unhandled exception. We would still recommend that a health check library handle exceptions that they know about and return `context.Registration.FailureStatus`. --- .../DbConnectionHealthCheck.cs | 4 +- .../HealthChecksSample/GCInfoHealthCheck.cs | 8 ++- .../SlowDependencyHealthCheck.cs | 6 +- .../HealthCheckOptions.cs | 3 - .../HealthCheckResult.cs | 45 ++++++++------ .../HealthReport.cs | 2 +- .../HealthReportEntry.cs | 3 +- .../HealthStatus.cs | 18 ++---- .../DbContextHealthCheck.cs | 4 +- .../DefaultHealthCheckService.cs | 8 +-- .../HealthChecksBuilderDelegateExtensions.cs | 28 ++------- .../HealthCheckMiddlewareTests.cs | 46 +++++++-------- .../DbContextHealthCheckTest.cs | 45 ++++++++++---- .../DefaultHealthCheckServiceTest.cs | 58 ++++++++++--------- .../HealthChecksBuilderTest.cs | 30 +++++----- .../HealthCheckPublisherHostedServiceTest.cs | 4 +- .../HealthReportTest.cs | 1 - 17 files changed, 158 insertions(+), 155 deletions(-) diff --git a/samples/HealthChecksSample/DbConnectionHealthCheck.cs b/samples/HealthChecksSample/DbConnectionHealthCheck.cs index 5a0ddcdd55..cb865f67c0 100644 --- a/samples/HealthChecksSample/DbConnectionHealthCheck.cs +++ b/samples/HealthChecksSample/DbConnectionHealthCheck.cs @@ -51,11 +51,11 @@ namespace HealthChecksSample } catch (DbException ex) { - return HealthCheckResult.Failed(exception: ex); + return new HealthCheckResult(status: context.Registration.FailureStatus, exception: ex); } } - return HealthCheckResult.Passed(); + return HealthCheckResult.Healthy(); } } } diff --git a/samples/HealthChecksSample/GCInfoHealthCheck.cs b/samples/HealthChecksSample/GCInfoHealthCheck.cs index c708f3ed3c..91519af452 100644 --- a/samples/HealthChecksSample/GCInfoHealthCheck.cs +++ b/samples/HealthChecksSample/GCInfoHealthCheck.cs @@ -65,13 +65,15 @@ namespace HealthChecksSample { "Gen2Collections", GC.CollectionCount(2) }, }; - // Report failure if the allocated memory is >= the threshold. Negated because true == success - var result = !(allocated >= options.Threshold); + // Report failure if the allocated memory is >= the threshold. + // + // Using context.Registration.FailureStatus means that the application developer can configure + // how they want failures to appear. + var result = allocated >= options.Threshold ? context.Registration.FailureStatus : HealthStatus.Healthy; return Task.FromResult(new HealthCheckResult( result, description: "reports degraded status if allocated bytes >= 1gb", - exception: null, data: data)); } } diff --git a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs index e319952cf0..e14aeb210c 100644 --- a/samples/HealthChecksSample/SlowDependencyHealthCheck.cs +++ b/samples/HealthChecksSample/SlowDependencyHealthCheck.cs @@ -21,10 +21,12 @@ namespace HealthChecksSample { if (_task.IsCompleted) { - return Task.FromResult(HealthCheckResult.Passed("Dependency is ready")); + return Task.FromResult(HealthCheckResult.Healthy("Dependency is ready")); } - return Task.FromResult(HealthCheckResult.Failed("Dependency is still initializing")); + return Task.FromResult(new HealthCheckResult( + status: context.Registration.FailureStatus, + description: "Dependency is still initializing")); } } } diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs index bf43676a5c..16b81c2b96 100644 --- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs +++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs @@ -33,9 +33,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { HealthStatus.Healthy, StatusCodes.Status200OK }, { HealthStatus.Degraded, StatusCodes.Status200OK }, { HealthStatus.Unhealthy, StatusCodes.Status503ServiceUnavailable }, - - // This means that a health check failed, so 500 is appropriate. This is an error. - { HealthStatus.Failed, StatusCodes.Status500InternalServerError }, }; /// diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs index 12cd38d08e..e01cb5aceb 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthCheckResult.cs @@ -14,16 +14,16 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks private static readonly IReadOnlyDictionary _emptyReadOnlyDictionary = new Dictionary(); /// - /// Creates a new with the specified values for , , - /// , and . + /// Creates a new with the specified values for , + /// , , and . /// - /// A value indicating the pass/fail status of the component that was checked. + /// A value indicating the status of the component that was checked. /// A human-readable description of the status of the component that was checked. /// An representing the exception that was thrown when checking for status (if any). /// Additional key-value pairs describing the health of the component. - public HealthCheckResult(bool result, string description, Exception exception, IReadOnlyDictionary data) + public HealthCheckResult(HealthStatus status, string description = null, Exception exception = null, IReadOnlyDictionary data = null) { - Result = result; + Status = status; Description = description; Exception = exception; Data = data ?? _emptyReadOnlyDictionary; @@ -45,33 +45,44 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public Exception Exception { get; } /// - /// Gets a value indicating the pass/fail status of the component that was checked. If true, then the component - /// is considered to have passed health validation. A false value will be mapped to the configured - /// by the health check system. + /// Gets a value indicating the status of the component that was checked. /// - public bool Result { get; } + public HealthStatus Status { get; } /// - /// Creates a representing a passing component. + /// Creates a representing a healthy component. /// - /// A representing a passing component. /// A human-readable description of the status of the component that was checked. Optional. /// Additional key-value pairs describing the health of the component. Optional. - public static HealthCheckResult Passed(string description = null, IReadOnlyDictionary data = null) + /// A representing a healthy component. + public static HealthCheckResult Healthy(string description = null, IReadOnlyDictionary data = null) { - return new HealthCheckResult(result: true, description, exception: null, data); + return new HealthCheckResult(status: HealthStatus.Healthy, description, exception: null, data); } + /// - /// Creates a representing an failing component. + /// Creates a representing a degraded component. /// /// A human-readable description of the status of the component that was checked. Optional. /// An representing the exception that was thrown when checking for status. Optional. /// Additional key-value pairs describing the health of the component. Optional. - /// A representing an failing component. - public static HealthCheckResult Failed(string description = null, Exception exception = null, IReadOnlyDictionary data = null) + /// A representing a degraged component. + public static HealthCheckResult Degraded(string description = null, Exception exception = null, IReadOnlyDictionary data = null) { - return new HealthCheckResult(result: false, description, exception, data); + return new HealthCheckResult(status: HealthStatus.Degraded, description, exception: null, data); + } + + /// + /// Creates a representing an unhealthy component. + /// + /// A human-readable description of the status of the component that was checked. Optional. + /// An representing the exception that was thrown when checking for status. Optional. + /// Additional key-value pairs describing the health of the component. Optional. + /// A representing an unhealthy component. + public static HealthCheckResult Unhealthy(string description = null, Exception exception = null, IReadOnlyDictionary data = null) + { + return new HealthCheckResult(status: HealthStatus.Unhealthy, description, exception, data); } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReport.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReport.cs index a0e6fac1cd..91ed798811 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReport.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReport.cs @@ -54,7 +54,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks currentValue = entry.Status; } - if (currentValue == HealthStatus.Failed) + if (currentValue == HealthStatus.Unhealthy) { // Game over, man! Game over! // (We hit the worst possible status, so there's no need to keep iterating) diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReportEntry.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReportEntry.cs index 2898c93aa4..6e7d6c6b8e 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReportEntry.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthReportEntry.cs @@ -52,8 +52,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public Exception Exception { get; } /// - /// Gets the health status of the component that was checked. The is based on the pass/fail value of - /// and the configured value of . + /// Gets the health status of the component that was checked. /// public HealthStatus Status { get; } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs index ad3f40a201..61b76d54fa 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/HealthStatus.cs @@ -8,10 +8,6 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// /// /// - /// A health status is derived the pass/fail result of an () - /// and the corresponding value of . - /// - /// /// A status of should be considered the default value for a failing health check. Application /// developers may configure a health check to report a different status as desired. /// @@ -23,23 +19,19 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public enum HealthStatus { /// - /// Indicates that an unexpected exception was thrown when running the health check. + /// Indicates that the health check determined that the component was unhealthy, or an unhandled + /// exception was thrown while executing the health check. /// - Failed = 0, - - /// - /// Indicates that the health check determined that the component was unhealthy. - /// - Unhealthy = 1, + Unhealthy = 0, /// /// Indicates that the health check determined that the component was in a degraded state. /// - Degraded = 2, + Degraded = 1, /// /// Indicates that the health check determined that the component was healthy. /// - Healthy = 3, + Healthy = 2, } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs index 9d581e89d3..7fa998f296 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs @@ -47,10 +47,10 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks if (await testQuery(_dbContext, cancellationToken)) { - return HealthCheckResult.Passed(); + return HealthCheckResult.Healthy(); } - return HealthCheckResult.Failed(); + return HealthCheckResult.Unhealthy(); } } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs index aa12d97418..d5d71d9cb4 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DefaultHealthCheckService.cs @@ -76,7 +76,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var duration = stopwatch.GetElapsedTime(); entry = new HealthReportEntry( - status: result.Result ? HealthStatus.Healthy : registration.FailureStatus, + status: result.Status, description: result.Description, duration: duration, exception: result.Exception, @@ -91,7 +91,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { var duration = stopwatch.GetElapsedTime(); entry = new HealthReportEntry( - status: HealthStatus.Failed, + status: HealthStatus.Unhealthy, description: ex.Message, duration: duration, exception: ex, @@ -212,10 +212,6 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks case HealthStatus.Unhealthy: _healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); break; - - case HealthStatus.Failed: - _healthCheckEndFailed(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); - break; } } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs index 70edd0649d..d7dfdd90ae 100644 --- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs @@ -19,10 +19,6 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . /// The name of the health check. - /// - /// The that should be reported when the health check reports a failure. If the provided value - /// is null, then will be reported. - /// /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . @@ -30,7 +26,6 @@ namespace Microsoft.Extensions.DependencyInjection this IHealthChecksBuilder builder, string name, Func check, - HealthStatus? failureStatus = null, IEnumerable tags = null) { if (builder == null) @@ -49,7 +44,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => Task.FromResult(check())); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); } /// @@ -57,10 +52,6 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . /// The name of the health check. - /// - /// The that should be reported when the health check reports a failure. If the provided value - /// is null, then will be reported. - /// /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . @@ -68,7 +59,6 @@ namespace Microsoft.Extensions.DependencyInjection this IHealthChecksBuilder builder, string name, Func check, - HealthStatus? failureStatus = null, IEnumerable tags = null) { if (builder == null) @@ -87,7 +77,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct))); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); } /// @@ -95,10 +85,6 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . /// The name of the health check. - /// - /// The that should be reported when the health check reports a failure. If the provided value - /// is null, then will be reported. - /// /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . @@ -106,7 +92,6 @@ namespace Microsoft.Extensions.DependencyInjection this IHealthChecksBuilder builder, string name, Func> check, - HealthStatus? failureStatus = null, IEnumerable tags = null) { if (builder == null) @@ -125,7 +110,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => check()); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); } /// @@ -133,10 +118,6 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . /// The name of the health check. - /// - /// The that should be reported when the health check reports a failure. If the provided value - /// is null, then will be reported. - /// /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . @@ -144,7 +125,6 @@ namespace Microsoft.Extensions.DependencyInjection this IHealthChecksBuilder builder, string name, Func> check, - HealthStatus? failureStatus = null, IEnumerable tags = null) { if (builder == null) @@ -163,7 +143,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => check(ct)); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); } } } diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs index a3036f9b70..9bcefafbaa 100644 --- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs @@ -110,9 +110,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!")) - .AddCheck("Bar", () => HealthCheckResult.Passed("A-ok!")) - .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!")); + .AddCheck("Foo", () => HealthCheckResult.Healthy("A-ok!")) + .AddCheck("Bar", () => HealthCheckResult.Healthy("A-ok!")) + .AddCheck("Baz", () => HealthCheckResult.Healthy("A-ok!")); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -135,9 +135,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddCheck("Foo", () => HealthCheckResult.Passed("A-ok!")) - .AddCheck("Bar", () => HealthCheckResult.Failed("Not so great."), failureStatus: HealthStatus.Degraded) - .AddCheck("Baz", () => HealthCheckResult.Passed("A-ok!")); + .AddCheck("Foo", () => HealthCheckResult.Healthy("A-ok!")) + .AddCheck("Bar", () => HealthCheckResult.Degraded("Not so great.")) + .AddCheck("Baz", () => HealthCheckResult.Healthy("A-ok!")); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -160,9 +160,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) - .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) - .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks } [Fact] - public async Task StatusCodeIs500IfCheckIsFailed() + public async Task StatusCodeIs503IfCheckHasUnhandledException() { var builder = new WebHostBuilder() .Configure(app => @@ -185,18 +185,18 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) .AddAsyncCheck("Bar", () => throw null) - .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); var response = await client.GetAsync("/health"); - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); - Assert.Equal("Failed", await response.Content.ReadAsStringAsync()); + Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync()); } [Fact] @@ -223,9 +223,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) - .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) - .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -252,9 +252,9 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) - .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("Pretty bad."))) - .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad."))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); @@ -357,10 +357,10 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks .ConfigureServices(services => { services.AddHealthChecks() - .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))) + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))) // Will get filtered out - .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Failed("A-ok!"))) - .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Passed("A-ok!"))); + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!"))) + .AddAsyncCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!"))); }); var server = new TestServer(builder); var client = server.CreateClient(); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs index 23b17d4939..b564a62795 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs @@ -14,9 +14,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { public class DbContextHealthCheckTest { - // Just testing pass here since it would be complicated to simulate a failure. All of that logic lives in EF anyway. + // Just testing healthy here since it would be complicated to simulate a failure. All of that logic lives in EF anyway. [Fact] - public async Task CheckAsync_DefaultTest_Pass() + public async Task CheckAsync_DefaultTest_Healthy() { // Arrange var services = CreateServices(); @@ -29,12 +29,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); // Assert - Assert.True(result.Result, "Health check passed"); + Assert.Equal(HealthStatus.Healthy, result.Status); } } [Fact] - public async Task CheckAsync_CustomTest_Pass() + public async Task CheckAsync_CustomTest_Healthy() { // Arrange var services = CreateServices(async (c, ct) => @@ -56,19 +56,18 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); // Assert - Assert.True(result.Result, "Health check passed"); + Assert.Equal(HealthStatus.Healthy, result.Status); } } - [Fact] - public async Task CheckAsync_CustomTest_Fail() + public async Task CheckAsync_CustomTest_Degraded() { // Arrange var services = CreateServices(async (c, ct) => { return 0 < await c.Blogs.CountAsync(); - }); + }, failureStatus: HealthStatus.Degraded); using (var scope = services.GetRequiredService().CreateScope()) { @@ -79,17 +78,41 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); // Assert - Assert.False(result.Result, "Health check failed"); + Assert.Equal(HealthStatus.Unhealthy, result.Status); } } - private static IServiceProvider CreateServices(Func> testQuery = null) + [Fact] + public async Task CheckAsync_CustomTest_Unhealthy() + { + // Arrange + var services = CreateServices(async (c, ct) => + { + return 0 < await c.Blogs.CountAsync(); + }, failureStatus: HealthStatus.Unhealthy); + + using (var scope = services.GetRequiredService().CreateScope()) + { + var registration = Assert.Single(services.GetRequiredService>().Value.Registrations); + var check = ActivatorUtilities.CreateInstance>(scope.ServiceProvider); + + // Act + var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); + + // Assert + Assert.Equal(HealthStatus.Unhealthy, result.Status); + } + } + + private static IServiceProvider CreateServices( + Func> testQuery = null, + HealthStatus failureStatus = HealthStatus.Unhealthy) { var serviceCollection = new ServiceCollection(); serviceCollection.AddDbContext(o => o.UseInMemoryDatabase("Test")); var builder = serviceCollection.AddHealthChecks(); - builder.AddDbContextCheck("test", HealthStatus.Degraded, new[] { "tag1", "tag2", }, testQuery); + builder.AddDbContextCheck("test", failureStatus, new[] { "tag1", "tag2", }, testQuery); return serviceCollection.BuildServiceProvider(); } } diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs index 6c74ddcdf0..9ab991204e 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DefaultHealthCheckServiceTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -26,11 +27,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks serviceCollection.AddLogging(); serviceCollection.AddOptions(); serviceCollection.AddHealthChecks() - .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) - .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) - .AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) - .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))) - .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Passed()))); + .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))) + .AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))) + .AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))) + .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))) + .AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy()))); var services = serviceCollection.BuildServiceProvider(); @@ -63,16 +64,25 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data))); - b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded); - b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception))); + b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); + b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); + b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); }); // Act var results = await service.CheckHealthAsync(); // Assert - Assert.Collection(results.Entries, + Assert.Collection( + results.Entries.OrderBy(kvp => kvp.Key), + actual => + { + Assert.Equal("DegradedCheck", actual.Key); + Assert.Equal(DegradedMessage, actual.Value.Description); + Assert.Equal(HealthStatus.Degraded, actual.Value.Status); + Assert.Null(actual.Value.Exception); + Assert.Empty(actual.Value.Data); + }, actual => { Assert.Equal("HealthyCheck", actual.Key); @@ -86,14 +96,6 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks }); }, actual => - { - Assert.Equal("DegradedCheck", actual.Key); - Assert.Equal(DegradedMessage, actual.Value.Description); - Assert.Equal(HealthStatus.Degraded, actual.Value.Status); - Assert.Null(actual.Value.Exception); - Assert.Empty(actual.Value.Data); - }, - actual => { Assert.Equal("UnhealthyCheck", actual.Key); Assert.Equal(UnhealthyMessage, actual.Value.Description); @@ -121,9 +123,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks var service = CreateHealthChecksService(b => { - b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Passed(HealthyMessage, data))); - b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Failed(DegradedMessage)), failureStatus: HealthStatus.Degraded); - b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Failed(UnhealthyMessage, exception))); + b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data))); + b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage))); + b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception))); }); // Act @@ -201,7 +203,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks insideCheck.SetResult(null); await Task.Delay(10000, ct); - return HealthCheckResult.Failed(); + return HealthCheckResult.Unhealthy(); }); }); @@ -218,7 +220,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks } [Fact] - public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToFailedResultAsync() + public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToUnhealthyResultAsync() { // Arrange var thrownException = new InvalidOperationException("Whoops!"); @@ -228,7 +230,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { b.AddAsyncCheck("Throws", ct => throw thrownException); b.AddAsyncCheck("Faults", ct => Task.FromException(faultedException)); - b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Passed())); + b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())); }); // Act @@ -241,14 +243,14 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { Assert.Equal("Throws", actual.Key); Assert.Equal(thrownException.Message, actual.Value.Description); - Assert.Equal(HealthStatus.Failed, actual.Value.Status); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); Assert.Same(thrownException, actual.Value.Exception); }, actual => { Assert.Equal("Faults", actual.Key); Assert.Equal(faultedException.Message, actual.Value.Description); - Assert.Equal(HealthStatus.Failed, actual.Value.Status); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); Assert.Same(faultedException, actual.Value.Exception); }, actual => @@ -278,7 +280,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks Assert.Equal("TestScope", item.Value); }); }); - return Task.FromResult(HealthCheckResult.Passed()); + return Task.FromResult(HealthCheckResult.Healthy()); }); var loggerFactory = new TestLoggerFactory(sink, enabled: true); @@ -398,7 +400,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - return Task.FromResult(HealthCheckResult.Passed()); + return Task.FromResult(HealthCheckResult.Healthy()); } } @@ -410,7 +412,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { { "name", context.Registration.Name }, }; - return Task.FromResult(HealthCheckResult.Passed(data: data)); + return Task.FromResult(HealthCheckResult.Healthy(data: data)); } } } diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs index c4974013c7..4235f152a2 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/DependencyInjection/HealthChecksBuilderTest.cs @@ -20,7 +20,7 @@ namespace Microsoft.Extensions.DependencyInjection // Arrange var instance = new DelegateHealthCheck((_) => { - return Task.FromResult(HealthCheckResult.Passed()); + return Task.FromResult(HealthCheckResult.Healthy()); }); var services = CreateServices(); @@ -112,9 +112,9 @@ namespace Microsoft.Extensions.DependencyInjection { // Arrange var services = CreateServices(); - services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, check: () => + services.AddHealthChecks().AddCheck("test", tags: new[] { "tag", }, check: () => { - return HealthCheckResult.Passed(); + return HealthCheckResult.Healthy(); }); var serviceProvider = services.BuildServiceProvider(); @@ -125,7 +125,7 @@ namespace Microsoft.Extensions.DependencyInjection // Assert var registration = Assert.Single(options.Registrations); Assert.Equal("test", registration.Name); - Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus); Assert.Equal(new[] { "tag", }, registration.Tags); Assert.IsType(registration.Factory(serviceProvider)); } @@ -137,8 +137,8 @@ namespace Microsoft.Extensions.DependencyInjection var services = CreateServices(); services.AddHealthChecks().AddCheck("test", (_) => { - return HealthCheckResult.Passed(); - }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + return HealthCheckResult.Degraded(); + }, tags: new[] { "tag", }); var serviceProvider = services.BuildServiceProvider(); @@ -148,7 +148,7 @@ namespace Microsoft.Extensions.DependencyInjection // Assert var registration = Assert.Single(options.Registrations); Assert.Equal("test", registration.Name); - Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus); Assert.Equal(new[] { "tag", }, registration.Tags); Assert.IsType(registration.Factory(serviceProvider)); } @@ -160,8 +160,8 @@ namespace Microsoft.Extensions.DependencyInjection var services = CreateServices(); services.AddHealthChecks().AddAsyncCheck("test", () => { - return Task.FromResult(HealthCheckResult.Passed()); - }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + return Task.FromResult(HealthCheckResult.Healthy()); + }, tags: new[] { "tag", }); var serviceProvider = services.BuildServiceProvider(); @@ -171,7 +171,7 @@ namespace Microsoft.Extensions.DependencyInjection // Assert var registration = Assert.Single(options.Registrations); Assert.Equal("test", registration.Name); - Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus); Assert.Equal(new[] { "tag", }, registration.Tags); Assert.IsType(registration.Factory(serviceProvider)); } @@ -183,8 +183,8 @@ namespace Microsoft.Extensions.DependencyInjection var services = CreateServices(); services.AddHealthChecks().AddAsyncCheck("test", (_) => { - return Task.FromResult(HealthCheckResult.Passed()); - }, failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }); + return Task.FromResult(HealthCheckResult.Unhealthy()); + }, tags: new[] { "tag", }); var serviceProvider = services.BuildServiceProvider(); @@ -194,7 +194,7 @@ namespace Microsoft.Extensions.DependencyInjection // Assert var registration = Assert.Single(options.Registrations); Assert.Equal("test", registration.Name); - Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus); Assert.Equal(new[] { "tag", }, registration.Tags); Assert.IsType(registration.Factory(serviceProvider)); } @@ -205,10 +205,10 @@ namespace Microsoft.Extensions.DependencyInjection var services = new ServiceCollection(); services .AddHealthChecks() - .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Passed())); + .AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy())); services .AddHealthChecks() - .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Passed())); + .AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy())); // Act var options = services.BuildServiceProvider().GetRequiredService>(); diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckPublisherHostedServiceTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckPublisherHostedServiceTest.cs index 49d57916eb..94687efcb8 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckPublisherHostedServiceTest.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckPublisherHostedServiceTest.cs @@ -448,8 +448,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks serviceCollection.AddOptions(); serviceCollection.AddLogging(); serviceCollection.AddHealthChecks() - .AddCheck("one", () => { return HealthCheckResult.Passed(); }) - .AddCheck("two", () => { return HealthCheckResult.Passed(); }); + .AddCheck("one", () => { return HealthCheckResult.Healthy(); }) + .AddCheck("two", () => { return HealthCheckResult.Healthy(); }); // Choosing big values for tests to make sure that we're not dependent on the defaults. // All of the tests that rely on the timer will set their own values for speed. diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs index 453398e639..07f8e5a8e3 100644 --- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthReportTest.cs @@ -13,7 +13,6 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks [InlineData(HealthStatus.Healthy)] [InlineData(HealthStatus.Degraded)] [InlineData(HealthStatus.Unhealthy)] - [InlineData(HealthStatus.Failed)] public void Status_MatchesWorstStatusInResults(HealthStatus status) { var result = new HealthReport(new Dictionary()