├── .gitignore ├── FunctionHealth.sln ├── LICENSE ├── README.md ├── samples └── Function.HealthCheck.SQL │ ├── Function.HealthCheck.SQL.csproj │ ├── HttpFunc.cs │ ├── Startup.cs │ └── host.json └── src └── FunctionHealthCheck ├── DefaultHealthCheckService.cs ├── FunctionHealthCheck.csproj ├── HealthCheckServiceFunctionExtension.cs └── HealthChecksBuilder.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc 265 | /samples/Function.HealthCheck.SQL/.vscode 266 | -------------------------------------------------------------------------------- /FunctionHealth.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30204.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionHealthCheck", "src\FunctionHealthCheck\FunctionHealthCheck.csproj", "{3031CB4F-AD17-4E19-825D-BBD2CB941C5D}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{91BEAED9-087E-40A5-9B1D-F3CFCF210DD3}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Function.HealthCheck.SQL", "samples\Function.HealthCheck.SQL\Function.HealthCheck.SQL.csproj", "{43C1BA38-9E0E-4963-B334-DFBB512E4B0C}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A7422A39-DBB4-4E28-8785-CF52E51E00A5}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {3031CB4F-AD17-4E19-825D-BBD2CB941C5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {3031CB4F-AD17-4E19-825D-BBD2CB941C5D}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {3031CB4F-AD17-4E19-825D-BBD2CB941C5D}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {3031CB4F-AD17-4E19-825D-BBD2CB941C5D}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {43C1BA38-9E0E-4963-B334-DFBB512E4B0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {43C1BA38-9E0E-4963-B334-DFBB512E4B0C}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {43C1BA38-9E0E-4963-B334-DFBB512E4B0C}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {43C1BA38-9E0E-4963-B334-DFBB512E4B0C}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | GlobalSection(NestedProjects) = preSolution 33 | {3031CB4F-AD17-4E19-825D-BBD2CB941C5D} = {A7422A39-DBB4-4E28-8785-CF52E51E00A5} 34 | {43C1BA38-9E0E-4963-B334-DFBB512E4B0C} = {91BEAED9-087E-40A5-9B1D-F3CFCF210DD3} 35 | EndGlobalSection 36 | GlobalSection(ExtensibilityGlobals) = postSolution 37 | SolutionGuid = {9042270F-CD78-4717-BEF0-BEA3F1C5939D} 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Function Health 2 | 3 | Azure Function Health is small library based on the aspnetcore HealthChecks feature. The traditional health checks registered in an aspnetcore API included the HealthCheckPublisherHostedService as a HostedService which is not possible or desired to run in an Azure Function. However there are benefits to included a health check in an Azure Function to test the depencies of your service. This library will allow you to register health checks for your dependencies and create an HTTP endpoint that can be used to monitor the health of your application. 4 | 5 | ## Health Checks 6 | 7 | There are a number of health checks that you can add to your Function App that have already been implemented. You can add any of the healthcheck defined here: https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks 8 | 9 | ## How it works 10 | 11 | 1. Add the following package to your Azure Function Project: Microsoft.Extensions.Diagnostics.HealthChecks 12 | 1. Add a Nuget package for the health check you would like to add: i.e. AspNetCore.HealthChecks.SqlServer 13 | 1. Add a reference to the FunctionHealthCheck project listed in this repository 14 | 1. Include a startup class to register the specific health checks required for your project. 15 | 16 | ```c# 17 | 18 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 19 | using Microsoft.Extensions.DependencyInjection; 20 | using Function.HealthCheck.SQL; 21 | using FunctionHealthCheck; 22 | using Microsoft.Extensions.Diagnostics.HealthChecks; 23 | using System; 24 | 25 | [assembly: FunctionsStartup(typeof(Startup))] 26 | namespace Function.HealthCheck.SQL 27 | { 28 | public class Startup : FunctionsStartup 29 | { 30 | public override void Configure(IFunctionsHostBuilder builder) 31 | { 32 | ConfigureServices(builder.Services); 33 | } 34 | 35 | public void ConfigureServices(IServiceCollection services) 36 | { 37 | if (services == null) 38 | { 39 | throw new ArgumentNullException(nameof(services)); 40 | } 41 | 42 | services.AddLogging(); 43 | 44 | services.AddFunctionHealthChecks() 45 | .AddSqlServer( 46 | connectionString: "Server=localhost;Database=yourdb;User Id=app;Password=test123"); 47 | 48 | } 49 | } 50 | } 51 | 52 | ``` 53 | 54 | 5. Add a health check endpoint for your application to expose: 55 | 56 | ```c# 57 | using System; 58 | using System.IO; 59 | using System.Threading.Tasks; 60 | using Microsoft.AspNetCore.Mvc; 61 | using Microsoft.Azure.WebJobs; 62 | using Microsoft.Azure.WebJobs.Extensions.Http; 63 | using Microsoft.AspNetCore.Http; 64 | using Microsoft.Extensions.Logging; 65 | using Newtonsoft.Json; 66 | using Microsoft.Extensions.Diagnostics.HealthChecks; 67 | 68 | namespace Function.HealthCheck.SQL 69 | { 70 | public class HttpFunc 71 | { 72 | private readonly HealthCheckService _healthCheck; 73 | public HttpFunc(HealthCheckService healthCheck) 74 | { 75 | _healthCheck = healthCheck; 76 | } 77 | 78 | [FunctionName("Heartbeat")] 79 | public async Task Heartbeat( 80 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "heartbeat")] HttpRequest req, 81 | ILogger log) 82 | { 83 | log.Log(LogLevel.Information, "Received heartbeat request"); 84 | 85 | var status = await _healthCheck.CheckHealthAsync(); 86 | 87 | return new OkObjectResult(Enum.GetName(typeof(HealthStatus), status.Status)); 88 | } 89 | 90 | } 91 | } 92 | 93 | ``` 94 | 95 | 6. Setup an external monitoring tool like Azure Monitor to create a ping test against this endpoint. https://docs.microsoft.com/en-us/azure/azure-monitor/app/monitor-web-app-availability 96 | -------------------------------------------------------------------------------- /samples/Function.HealthCheck.SQL/Function.HealthCheck.SQL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | sql_func_health 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | Never 23 | 24 | 25 | -------------------------------------------------------------------------------- /samples/Function.HealthCheck.SQL/HttpFunc.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.Http; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Logging; 9 | using Newtonsoft.Json; 10 | using Microsoft.Extensions.Diagnostics.HealthChecks; 11 | 12 | namespace Function.HealthCheck.SQL 13 | { 14 | public class HttpFunc 15 | { 16 | private readonly HealthCheckService _healthCheck; 17 | public HttpFunc(HealthCheckService healthCheck) 18 | { 19 | _healthCheck = healthCheck; 20 | } 21 | 22 | [FunctionName("Heartbeat")] 23 | public async Task Heartbeat( 24 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "heartbeat")] HttpRequest req, 25 | ILogger log) 26 | { 27 | log.Log(LogLevel.Information, "Received heartbeat request"); 28 | 29 | var status = await _healthCheck.CheckHealthAsync(); 30 | 31 | return new OkObjectResult(Enum.GetName(typeof(HealthStatus), status.Status)); 32 | } 33 | 34 | [FunctionName("HttpFunc")] 35 | public static async Task Run( 36 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, 37 | ILogger log) 38 | { 39 | log.LogInformation("C# HTTP trigger function processed a request."); 40 | 41 | string name = req.Query["name"]; 42 | 43 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 44 | dynamic data = JsonConvert.DeserializeObject(requestBody); 45 | name = name ?? data?.name; 46 | 47 | string responseMessage = string.IsNullOrEmpty(name) 48 | ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response." 49 | : $"Hello, {name}. This HTTP triggered function executed successfully."; 50 | 51 | return new OkObjectResult(responseMessage); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /samples/Function.HealthCheck.SQL/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Function.HealthCheck.SQL; 4 | using FunctionHealthCheck; 5 | using Microsoft.Extensions.Diagnostics.HealthChecks; 6 | using System; 7 | 8 | [assembly: FunctionsStartup(typeof(Startup))] 9 | namespace Function.HealthCheck.SQL 10 | { 11 | public class Startup : FunctionsStartup 12 | { 13 | public override void Configure(IFunctionsHostBuilder builder) 14 | { 15 | ConfigureServices(builder.Services); 16 | } 17 | 18 | public void ConfigureServices(IServiceCollection services) 19 | { 20 | if (services == null) 21 | { 22 | throw new ArgumentNullException(nameof(services)); 23 | } 24 | 25 | services.AddLogging(); 26 | 27 | services.AddFunctionHealthChecks() 28 | .AddSqlServer( 29 | connectionString: "Server=localhost;Database=yourdb;User Id=app;Password=test123"); 30 | 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/Function.HealthCheck.SQL/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingExcludedTypes": "Request", 6 | "samplingSettings": { 7 | "isEnabled": true 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/FunctionHealthCheck/DefaultHealthCheckService.cs: -------------------------------------------------------------------------------- 1 | //Copyright(c) .NET Foundation and Contributors 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Options; 6 | using System; 7 | using System.Collections; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | 15 | namespace FunctionHealthCheck 16 | { 17 | internal class DefaultHealthCheckService : HealthCheckService 18 | { 19 | private readonly IServiceScopeFactory _scopeFactory; 20 | private readonly IOptions _options; 21 | private readonly ILogger _logger; 22 | 23 | public DefaultHealthCheckService( 24 | IServiceScopeFactory scopeFactory, 25 | IOptions options, 26 | ILogger logger) 27 | { 28 | _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); 29 | _options = options ?? throw new ArgumentNullException(nameof(options)); 30 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 31 | 32 | // We're specifically going out of our way to do this at startup time. We want to make sure you 33 | // get any kind of health-check related error as early as possible. Waiting until someone 34 | // actually tries to **run** health checks would be real baaaaad. 35 | ValidateRegistrations(_options.Value.Registrations); 36 | } 37 | public override async Task CheckHealthAsync( 38 | Func predicate, 39 | CancellationToken cancellationToken = default) 40 | { 41 | var registrations = _options.Value.Registrations; 42 | 43 | using (var scope = _scopeFactory.CreateScope()) 44 | { 45 | var context = new HealthCheckContext(); 46 | var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); 47 | 48 | var totalTime = ValueStopwatch.StartNew(); 49 | Log.HealthCheckProcessingBegin(_logger); 50 | 51 | foreach (var registration in registrations) 52 | { 53 | if (predicate != null && !predicate(registration)) 54 | { 55 | continue; 56 | } 57 | 58 | cancellationToken.ThrowIfCancellationRequested(); 59 | 60 | var healthCheck = registration.Factory(scope.ServiceProvider); 61 | 62 | var stopwatch = ValueStopwatch.StartNew(); 63 | context.Registration = registration; 64 | 65 | Log.HealthCheckBegin(_logger, registration); 66 | 67 | HealthReportEntry entry; 68 | try 69 | { 70 | var result = await healthCheck.CheckHealthAsync(context, cancellationToken); 71 | var duration = stopwatch.Elapsed; 72 | 73 | entry = new HealthReportEntry( 74 | status: result.Status, 75 | description: result.Description, 76 | duration: duration, 77 | exception: result.Exception, 78 | data: result.Data); 79 | 80 | Log.HealthCheckEnd(_logger, registration, entry, duration); 81 | Log.HealthCheckData(_logger, registration, entry); 82 | } 83 | 84 | // Allow cancellation to propagate. 85 | catch (Exception ex) when (ex as OperationCanceledException == null) 86 | { 87 | var duration = stopwatch.Elapsed; 88 | entry = new HealthReportEntry( 89 | status: HealthStatus.Unhealthy, 90 | description: ex.Message, 91 | duration: duration, 92 | exception: ex, 93 | data: null); 94 | 95 | Log.HealthCheckError(_logger, registration, ex, duration); 96 | } 97 | 98 | entries[registration.Name] = entry; 99 | } 100 | 101 | var totalElapsedTime = totalTime.Elapsed; 102 | var report = new HealthReport(entries, totalElapsedTime); 103 | Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime); 104 | return report; 105 | } 106 | } 107 | 108 | private static void ValidateRegistrations(IEnumerable registrations) 109 | { 110 | // Scan the list for duplicate names to provide a better error if there are duplicates. 111 | var duplicateNames = registrations 112 | .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) 113 | .Where(g => g.Count() > 1) 114 | .Select(g => g.Key) 115 | .ToList(); 116 | 117 | if (duplicateNames.Count > 0) 118 | { 119 | throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(registrations)); 120 | } 121 | } 122 | 123 | internal static class EventIds 124 | { 125 | public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin"); 126 | public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd"); 127 | 128 | public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin"); 129 | public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd"); 130 | public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError"); 131 | public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData"); 132 | } 133 | 134 | private static class Log 135 | { 136 | private static readonly Action _healthCheckProcessingBegin = LoggerMessage.Define( 137 | LogLevel.Debug, 138 | EventIds.HealthCheckProcessingBegin, 139 | "Running health checks"); 140 | 141 | private static readonly Action _healthCheckProcessingEnd = LoggerMessage.Define( 142 | LogLevel.Debug, 143 | EventIds.HealthCheckProcessingEnd, 144 | "Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}"); 145 | 146 | private static readonly Action _healthCheckBegin = LoggerMessage.Define( 147 | LogLevel.Debug, 148 | EventIds.HealthCheckBegin, 149 | "Running health check {HealthCheckName}"); 150 | 151 | // These are separate so they can have different log levels 152 | private static readonly string HealthCheckEndText = "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'"; 153 | 154 | private static readonly Action _healthCheckEndHealthy = LoggerMessage.Define( 155 | LogLevel.Debug, 156 | EventIds.HealthCheckEnd, 157 | HealthCheckEndText); 158 | 159 | private static readonly Action _healthCheckEndDegraded = LoggerMessage.Define( 160 | LogLevel.Warning, 161 | EventIds.HealthCheckEnd, 162 | HealthCheckEndText); 163 | 164 | private static readonly Action _healthCheckEndUnhealthy = LoggerMessage.Define( 165 | LogLevel.Error, 166 | EventIds.HealthCheckEnd, 167 | HealthCheckEndText); 168 | 169 | private static readonly Action _healthCheckEndFailed = LoggerMessage.Define( 170 | LogLevel.Error, 171 | EventIds.HealthCheckEnd, 172 | HealthCheckEndText); 173 | 174 | private static readonly Action _healthCheckError = LoggerMessage.Define( 175 | LogLevel.Error, 176 | EventIds.HealthCheckError, 177 | "Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms"); 178 | 179 | public static void HealthCheckProcessingBegin(ILogger logger) 180 | { 181 | _healthCheckProcessingBegin(logger, null); 182 | } 183 | 184 | public static void HealthCheckProcessingEnd(ILogger logger, HealthStatus status, TimeSpan duration) 185 | { 186 | _healthCheckProcessingEnd(logger, duration.TotalMilliseconds, status, null); 187 | } 188 | 189 | public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration) 190 | { 191 | _healthCheckBegin(logger, registration.Name, null); 192 | } 193 | 194 | public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration) 195 | { 196 | switch (entry.Status) 197 | { 198 | case HealthStatus.Healthy: 199 | _healthCheckEndHealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); 200 | break; 201 | 202 | case HealthStatus.Degraded: 203 | _healthCheckEndDegraded(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); 204 | break; 205 | 206 | case HealthStatus.Unhealthy: 207 | _healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null); 208 | break; 209 | } 210 | } 211 | 212 | public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration) 213 | { 214 | _healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception); 215 | } 216 | 217 | public static void HealthCheckData(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry) 218 | { 219 | if (entry.Data.Count > 0 && logger.IsEnabled(LogLevel.Debug)) 220 | { 221 | logger.Log( 222 | LogLevel.Debug, 223 | EventIds.HealthCheckData, 224 | new HealthCheckDataLogValue(registration.Name, entry.Data), 225 | null, 226 | (state, ex) => state.ToString()); 227 | } 228 | } 229 | } 230 | 231 | internal class HealthCheckDataLogValue : IReadOnlyList> 232 | { 233 | private readonly string _name; 234 | private readonly List> _values; 235 | 236 | private string _formatted; 237 | 238 | public HealthCheckDataLogValue(string name, IReadOnlyDictionary values) 239 | { 240 | _name = name; 241 | _values = values.ToList(); 242 | 243 | // We add the name as a kvp so that you can filter by health check name in the logs. 244 | // This is the same parameter name used in the other logs. 245 | _values.Add(new KeyValuePair("HealthCheckName", name)); 246 | } 247 | 248 | public KeyValuePair this[int index] 249 | { 250 | get 251 | { 252 | if (index < 0 || index >= Count) 253 | { 254 | throw new IndexOutOfRangeException(nameof(index)); 255 | } 256 | 257 | return _values[index]; 258 | } 259 | } 260 | 261 | public int Count => _values.Count; 262 | 263 | public IEnumerator> GetEnumerator() 264 | { 265 | return _values.GetEnumerator(); 266 | } 267 | 268 | IEnumerator IEnumerable.GetEnumerator() 269 | { 270 | return _values.GetEnumerator(); 271 | } 272 | 273 | public override string ToString() 274 | { 275 | if (_formatted == null) 276 | { 277 | var builder = new StringBuilder(); 278 | builder.AppendLine($"Health check data for {_name}:"); 279 | 280 | var values = _values; 281 | for (var i = 0; i < values.Count; i++) 282 | { 283 | var kvp = values[i]; 284 | builder.Append(" "); 285 | builder.Append(kvp.Key); 286 | builder.Append(": "); 287 | 288 | builder.AppendLine(kvp.Value?.ToString()); 289 | } 290 | 291 | _formatted = builder.ToString(); 292 | } 293 | 294 | return _formatted; 295 | } 296 | } 297 | } 298 | 299 | internal struct ValueStopwatch 300 | { 301 | private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; 302 | private long value; 303 | 304 | /// 305 | /// Starts a new instance. 306 | /// 307 | /// A new, running stopwatch. 308 | public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); 309 | 310 | private ValueStopwatch(long timestamp) 311 | { 312 | this.value = timestamp; 313 | } 314 | 315 | /// 316 | /// Returns true if this instance is running or false otherwise. 317 | /// 318 | public bool IsRunning => this.value > 0; 319 | 320 | /// 321 | /// Returns the elapsed time. 322 | /// 323 | public TimeSpan Elapsed => TimeSpan.FromTicks(this.ElapsedTicks); 324 | 325 | /// 326 | /// Returns the elapsed ticks. 327 | /// 328 | public long ElapsedTicks 329 | { 330 | get 331 | { 332 | // A positive timestamp value indicates the start time of a running stopwatch, 333 | // a negative value indicates the negative total duration of a stopped stopwatch. 334 | var timestamp = this.value; 335 | 336 | long delta; 337 | if (this.IsRunning) 338 | { 339 | // The stopwatch is still running. 340 | var start = timestamp; 341 | var end = Stopwatch.GetTimestamp(); 342 | delta = end - start; 343 | } 344 | else 345 | { 346 | // The stopwatch has been stopped. 347 | delta = -timestamp; 348 | } 349 | 350 | return (long)(delta * TimestampToTicks); 351 | } 352 | } 353 | 354 | /// 355 | /// Gets the raw counter value for this instance. 356 | /// 357 | /// 358 | /// A positive timestamp value indicates the start time of a running stopwatch, 359 | /// a negative value indicates the negative total duration of a stopped stopwatch. 360 | /// 361 | /// The raw counter value. 362 | public long GetRawTimestamp() => this.value; 363 | 364 | /// 365 | /// Starts the stopwatch. 366 | /// 367 | public void Start() 368 | { 369 | var timestamp = this.value; 370 | 371 | // If already started, do nothing. 372 | if (this.IsRunning) return; 373 | 374 | // Stopwatch is stopped, therefore value is zero or negative. 375 | // Add the negative value to the current timestamp to start the stopwatch again. 376 | var newValue = Stopwatch.GetTimestamp() + timestamp; 377 | if (newValue == 0) newValue = 1; 378 | this.value = newValue; 379 | } 380 | 381 | /// 382 | /// Restarts this stopwatch, beginning from zero time elapsed. 383 | /// 384 | public void Restart() => this.value = Stopwatch.GetTimestamp(); 385 | 386 | /// 387 | /// Stops this stopwatch. 388 | /// 389 | public void Stop() 390 | { 391 | var timestamp = this.value; 392 | 393 | // If already stopped, do nothing. 394 | if (!this.IsRunning) return; 395 | 396 | var end = Stopwatch.GetTimestamp(); 397 | var delta = end - timestamp; 398 | 399 | this.value = -delta; 400 | } 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/FunctionHealthCheck/FunctionHealthCheck.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/FunctionHealthCheck/HealthCheckServiceFunctionExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace FunctionHealthCheck 7 | { 8 | /// 9 | /// Provides extension methods for registering in an . 10 | /// 11 | public static class HealthCheckServiceFunctionExtension 12 | { 13 | /// 14 | /// Adds the to the container, using the provided delegate to register 15 | /// health checks. 16 | /// 17 | /// 18 | /// This operation is idempotent - multiple invocations will still only result in a single 19 | /// instance in the . It can be invoked 20 | /// multiple times in order to get access to the in multiple places. 21 | /// 22 | /// The to add the to. 23 | /// An instance of from which health checks can be registered. 24 | public static IHealthChecksBuilder AddFunctionHealthChecks(this IServiceCollection services) 25 | { 26 | services.TryAddSingleton(); 27 | return new HealthChecksBuilder(services); 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/FunctionHealthCheck/HealthChecksBuilder.cs: -------------------------------------------------------------------------------- 1 | //Copyright(c) .NET Foundation and Contributors 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace FunctionHealthCheck 9 | { 10 | internal class HealthChecksBuilder : IHealthChecksBuilder 11 | { 12 | public HealthChecksBuilder(IServiceCollection services) 13 | { 14 | Services = services; 15 | } 16 | 17 | public IServiceCollection Services { get; } 18 | 19 | public IHealthChecksBuilder Add(HealthCheckRegistration registration) 20 | { 21 | if (registration == null) 22 | { 23 | throw new ArgumentNullException(nameof(registration)); 24 | } 25 | 26 | Services.Configure(options => 27 | { 28 | options.Registrations.Add(registration); 29 | }); 30 | 31 | return this; 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------