Creating health check dashboard using Warden
Hello! Today’s post for Get Noticed competition will be about creating a dynamic dashboard for health checks. The goal of this is to have one place when you can check if every system/environment you maintain actually works. To do this I’m gonna use Warden, which is a library created especially for this task, by the last year winner of Get Noticed competition, Piotr Gankiewicz. Warden support a lot of different types of checks, can work in real-time and even send notifications if something is wrong. For the first version though, I’ll create a simple website with web checks only, something to make quick glance of an eye on, to be assured everything is ok.
Let’s start with settings. Checks can be dynamically added/deleted by the user, so I need to have a collection of them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class HealthCheckSettings : ISettings<HealthCheckSettings> { public SingleHealthCheckSettings[] Settings { get; set; } public HealthCheckSettings WithDefaultValues() { Settings = new SingleHealthCheckSettings[0]; return this; } } public class SingleHealthCheckSettings { public string Url { get; set; } public string Name { get; set; } } |
And dynamic settings view created using Vue.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
<style> .single-health-check { margin: 0.35em 0.7em 0.35em 0.7em !important; } .single-health-check input { margin-right: 0.5em; } </style> <div id="app"> <div v-for="(setting, key) in settings.settings"> <form class="form-inline"> <div class="form-group single-health-check"> <label for="name">Name</label> <input type="text" class="form-control" id="name" required="required" v-model="setting.name"> <label for="url">Url</label> <input type="url" class="form-control" id="url" required="required" v-model="setting.url"> <i class="fa fa-trash-o" aria-hidden="true" v-on:click="removeHealthCheck(key)"></i> </div> </form> </div> <button class="btn btn-default" v-on:click.prevent="addWebHealthCheck">Add web health check</button> <button class="btn btn-default" v-on:click.prevent="saveSettings">Save</button> <span v-if="saved" class="label label-success">saved</span> </div> <script type="text/javascript"> var teamCitySettingsVM = new Vue({ el: "#app", data: { settings: { settings: [] }, saved: false }, methods: { saveSettings: function () { this.saved = false; this.$http.post('@Url.Action("SaveSettings", "HealthCheck")', this.settings).then(function(response) { this.saved = true; }); }, addWebHealthCheck: function() { this.settings.settings.push({ name: "", url: "" }); }, removeHealthCheck: function (key) { this.settings.settings.splice(key, 1); }, }, created: function() { this.$http.get('@Url.Action("GetSettings", "HealthCheck")').then(function(response) { this.settings = response.body; }); }, }); </script> |
Which creates this screen:
Ok, we know what to check, now the question is how to do it? This is the job of WardenService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TeamScreen.Plugin.Base.Extensions; using TeamScreen.Plugin.HealthCheck.Models; using Warden; using Warden.Core; using Warden.Watchers.Web; namespace TeamScreen.Plugin.HealthCheck.Integration { public interface IWardenService { Task<List<IWardenCheckResult>> DoHealthChecks(IEnumerable<SingleHealthCheckSettings> settings); } public class WardenService : IWardenService { public async Task<List<IWardenCheckResult>> DoHealthChecks(IEnumerable<SingleHealthCheckSettings> settings) { var results = new List<IWardenCheckResult>(); var configuration = Configure(settings, results); var warden = WardenInstance.Create(configuration); await warden.StartAsync(); return results; } private WardenConfiguration Configure(IEnumerable<SingleHealthCheckSettings> settings, List<IWardenCheckResult> results) { var builder = WardenConfiguration .Create(); settings .Where(x => x.Name.IsNotNullAndNotEmpty() && x.Url.IsNotNullAndNotEmpty()) .Select(BuildWebWatcher) .ForEach(x => builder.AddWatcher(x)); var configuration = builder .SetHooks(x => x.OnIterationCompleted(iteration => results.AddRange(iteration.Results))) .RunOnlyOnce() .Build(); return configuration; } private WebWatcher BuildWebWatcher(SingleHealthCheckSettings settings) { var configuration = WebWatcherConfiguration.Create(settings.Url).Build(); return WebWatcher.Create(settings.Name, configuration); } } } |
WardenService task is to create WardenConfiguration object from the list of settings and after that run it once and gather results. The Warden itself is not prepared in my opinion to do such a thing – it is more inclined to run continuously, that’s why I need to do some hacks – like passing the list of future results.
When WardenService is created, we need a controller which is gonna use it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using TeamScreen.Data.Services; using TeamScreen.Plugin.HealthCheck.Integration; using TeamScreen.Plugin.HealthCheck.Models; namespace TeamScreen.Plugin.HealthCheck.Controllers { public class HealthCheckController : Controller { private readonly ISettingsService _settingsService; private readonly IWardenService _wardenService; public HealthCheckController(ISettingsService settingsService, IWardenService wardenService) { _settingsService = settingsService; _wardenService = wardenService; } public async Task<PartialViewResult> Content() { var settings = await _settingsService.Get<HealthCheckSettings>(Const.PluginName); var results = await _wardenService.DoHealthChecks(settings.Settings); return PartialView(results); } public PartialViewResult Settings() { return PartialView(); } public async Task<JsonResult> GetSettings() { var settings = await _settingsService.Get<HealthCheckSettings>(Const.PluginName); return Json(settings); } [HttpPost] public async Task SaveSettings([FromBody]HealthCheckSettings settings) { await _settingsService.Set(Const.PluginName, settings); } } } |
Finally, we need actual plugin view returned from Content method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
@model List<Warden.IWardenCheckResult> <style> .check { width: 10em; font-size: 4em; } .icon { display: inline-block; vertical-align: middle; } </style> <div> <h1>Health check</h1> <hr /> @if (Model.Any(x => !x.IsValid)) { <div class="panel panel-default"> <div class="panel-heading">Errors</div> <div class="panel-body"> <ul class="list-inline"> @foreach (var check in Model.Where(x => !x.IsValid)) { <li> <div class="alert alert-danger check"> <span> @check.WatcherCheckResult.WatcherName <div class="pull-right"> <i class="fa fa-exclamation-triangle icon" aria-hidden="true"></i> </div> </span> </div> </li> } </ul> </div> </div> } else { <div class="jumbotron"> <h1>Everything works!</h1> </div> } <div class="panel panel-default"> <div class="panel-body"> <ul class="list-inline"> @foreach (var check in Model.Where(x => x.IsValid)) { <li> <div class="alert alert-success check"> <span> @check.WatcherCheckResult.WatcherName <div class="pull-right"> <i class="fa fa-check icon" aria-hidden="true"></i> </div> </span> </div> </li> } </ul> </div> </div> </div> |
Which produces this view:
With that, we finally have a new plugin for TeamScreen. Full source code is available on GitHub. Thanks for reading and see you next time!
One thought on “Creating health check dashboard using Warden”