diff --git a/Src/Metrics/Core/HealthCheck.cs b/Src/Metrics/Core/HealthCheck.cs index d1a30e0a..da546802 100644 --- a/Src/Metrics/Core/HealthCheck.cs +++ b/Src/Metrics/Core/HealthCheck.cs @@ -8,35 +8,39 @@ public struct Result { public readonly string Name; public readonly HealthCheckResult Check; + public readonly MetricTags Tags; - public Result(string name, HealthCheckResult check) + public Result(string name, HealthCheckResult check, MetricTags tags = default(MetricTags)) { this.Name = name; this.Check = check; + this.Tags = tags; } } private readonly Func check; - protected HealthCheck(string name) - : this(name, () => { }) + protected HealthCheck(string name, MetricTags tags = default(MetricTags)) + : this(name, () => { }, tags) { } - public HealthCheck(string name, Action check) - : this(name, () => { check(); return string.Empty; }) + public HealthCheck(string name, Action check, MetricTags tags = default(MetricTags)) + : this(name, () => { check(); return string.Empty; }, tags) { } - public HealthCheck(string name, Func check) - : this(name, () => HealthCheckResult.Healthy(check())) + public HealthCheck(string name, Func check, MetricTags tags = default(MetricTags)) + : this(name, () => HealthCheckResult.Healthy(check()), tags) { } - public HealthCheck(string name, Func check) + public HealthCheck(string name, Func check, MetricTags tags = default(MetricTags)) { this.Name = name; this.check = check; + this.Tags = tags; } public string Name { get; } + public MetricTags Tags { get; set; } protected virtual HealthCheckResult Check() { @@ -47,11 +51,11 @@ public Result Execute() { try { - return new Result(this.Name, this.Check()); + return new Result(this.Name, this.Check(), Tags); } catch (Exception x) { - return new Result(this.Name, HealthCheckResult.Unhealthy(x)); + return new Result(this.Name, HealthCheckResult.Unhealthy(x), Tags); } } } diff --git a/Src/Metrics/HealthChecks.cs b/Src/Metrics/HealthChecks.cs index 5428f21b..5c9c396b 100644 --- a/Src/Metrics/HealthChecks.cs +++ b/Src/Metrics/HealthChecks.cs @@ -46,9 +46,10 @@ public static class HealthChecks /// /// Name of the health check. /// Action to execute. - public static void RegisterHealthCheck(string name, Action check) + /// Optional set of tags that can be associated with the health check. + public static void RegisterHealthCheck(string name, Action check, MetricTags tags = default(MetricTags)) { - RegisterHealthCheck(new HealthCheck(name, check)); + RegisterHealthCheck(new HealthCheck(name, check, tags)); } /// @@ -57,9 +58,10 @@ public static void RegisterHealthCheck(string name, Action check) /// /// Name of the health check. /// Function to execute. - public static void RegisterHealthCheck(string name, Func check) + /// Optional set of tags that can be associated with the health check. + public static void RegisterHealthCheck(string name, Func check, MetricTags tags = default(MetricTags)) { - RegisterHealthCheck(new HealthCheck(name, check)); + RegisterHealthCheck(new HealthCheck(name, check, tags)); } /// @@ -68,9 +70,10 @@ public static void RegisterHealthCheck(string name, Func check) /// /// Name of the health check. /// Function to execute - public static void RegisterHealthCheck(string name, Func check) + /// Optional set of tags that can be associated with the health check. + public static void RegisterHealthCheck(string name, Func check, MetricTags tags = default(MetricTags)) { - RegisterHealthCheck(new HealthCheck(name, check)); + RegisterHealthCheck(new HealthCheck(name, check, tags)); } /// diff --git a/Src/Metrics/Json/JsonHealthChecks.cs b/Src/Metrics/Json/JsonHealthChecksV1.cs similarity index 87% rename from Src/Metrics/Json/JsonHealthChecks.cs rename to Src/Metrics/Json/JsonHealthChecksV1.cs index b6324563..7f817f5f 100644 --- a/Src/Metrics/Json/JsonHealthChecks.cs +++ b/Src/Metrics/Json/JsonHealthChecksV1.cs @@ -5,7 +5,7 @@ namespace Metrics.Json { - public class JsonHealthChecks + public class JsonHealthChecksV1 { public const int Version = 1; public const string HealthChecksMimeType = "application/vnd.metrics.net.v1.health+json"; @@ -15,26 +15,26 @@ public class JsonHealthChecks public static string BuildJson(HealthStatus status) { return BuildJson(status, Clock.Default, indented: false); } public static string BuildJson(HealthStatus status, Clock clock, bool indented = true) { - return new JsonHealthChecks() + return new JsonHealthChecksV1() .AddVersion(Version) .AddTimestamp(Clock.Default) .AddObject(status) .GetJson(indented); } - public JsonHealthChecks AddVersion(int version) + public JsonHealthChecksV1 AddVersion(int version) { root.Add(new JsonProperty("Version", version.ToString(CultureInfo.InvariantCulture))); return this; } - public JsonHealthChecks AddTimestamp(Clock clock) + public JsonHealthChecksV1 AddTimestamp(Clock clock) { root.Add(new JsonProperty("Timestamp", Clock.FormatTimestamp(clock.UTCDateTime))); return this; } - public JsonHealthChecks AddObject(HealthStatus status) + public JsonHealthChecksV1 AddObject(HealthStatus status) { var properties = new List() { new JsonProperty("IsHealthy", status.IsHealthy) }; var unhealty = status.Results.Where(r => !r.Check.IsHealthy) diff --git a/Src/Metrics/Json/JsonHealthChecksV2.cs b/Src/Metrics/Json/JsonHealthChecksV2.cs new file mode 100644 index 00000000..a276a27d --- /dev/null +++ b/Src/Metrics/Json/JsonHealthChecksV2.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Metrics.Core; +using Metrics.Utils; + +namespace Metrics.Json +{ + public class JsonHealthChecksV2 + { + public const int Version = 2; + public const string HealthChecksMimeType = "application/vnd.metrics.net.v2.health+json"; + + private readonly List root = new List(); + + public static string BuildJson(HealthStatus status) { return BuildJson(status, Clock.Default, indented: false); } + public static string BuildJson(HealthStatus status, Clock clock, bool indented = true) + { + return new JsonHealthChecksV2() + .AddVersion(Version) + .AddTimestamp(Clock.Default) + .AddObject(status) + .GetJson(indented); + } + + public JsonHealthChecksV2 AddVersion(int version) + { + root.Add(new JsonProperty("Version", version.ToString(CultureInfo.InvariantCulture))); + return this; + } + + public JsonHealthChecksV2 AddTimestamp(Clock clock) + { + root.Add(new JsonProperty("Timestamp", Clock.FormatTimestamp(clock.UTCDateTime))); + return this; + } + + public JsonHealthChecksV2 AddObject(HealthStatus status) + { + var properties = new List() { new JsonProperty("IsHealthy", status.IsHealthy) }; + var unhealty = status.Results.Where(r => !r.Check.IsHealthy); + properties.Add(new JsonProperty("Unhealthy", CreateHealthJsonObject(unhealty))); + var healthy = status.Results.Where(r => r.Check.IsHealthy); + properties.Add(new JsonProperty("Healthy", CreateHealthJsonObject(healthy))); + this.root.AddRange(properties); + return this; + } + + private IEnumerable CreateHealthJsonObject(IEnumerable results) + { + return results.Select(r => new JsonObject(new List() + { + new JsonProperty("Name", r.Name), + new JsonProperty("Message", r.Check.Message), + new JsonProperty("Tags", r.Tags.Tags) + })); + } + + public string GetJson(bool indented = true) + { + return new JsonObject(root).AsJson(indented); + } + } +} diff --git a/Src/Metrics/Metrics.csproj b/Src/Metrics/Metrics.csproj index c47211b1..df8fb654 100644 --- a/Src/Metrics/Metrics.csproj +++ b/Src/Metrics/Metrics.csproj @@ -88,6 +88,7 @@ + @@ -96,7 +97,7 @@ - + diff --git a/Src/Metrics/Reporters/EndpointReporterConfig.cs b/Src/Metrics/Reporters/EndpointReporterConfig.cs index f53b6ed3..5d3ca5c6 100644 --- a/Src/Metrics/Reporters/EndpointReporterConfig.cs +++ b/Src/Metrics/Reporters/EndpointReporterConfig.cs @@ -15,22 +15,62 @@ public static MetricsEndpointReports WithTextReport(this MetricsEndpointReports return reports.WithEndpointReport(endpoint, (d, h, c) => new MetricsEndpointResponse(StringReport.RenderMetrics(d, h), "text/plain")); } + #region HealthChecks + + public static MetricsEndpointReports WithJsonHealthV1Report(this MetricsEndpointReports reports, string endpoint, bool alwaysReturnOkStatusCode = false) + { + return reports.WithEndpointReport(endpoint, (d, h, r) => GetHealthResponse(h, JsonHealthChecksV1.BuildJson, JsonHealthChecksV1.HealthChecksMimeType, alwaysReturnOkStatusCode)); + } + + public static MetricsEndpointReports WithJsonHealthV2Report(this MetricsEndpointReports reports, string endpoint, bool alwaysReturnOkStatusCode = false) + { + return reports.WithEndpointReport(endpoint, (d, h, r) => GetHealthResponse(h, JsonHealthChecksV2.BuildJson, JsonHealthChecksV2.HealthChecksMimeType, alwaysReturnOkStatusCode)); + } + public static MetricsEndpointReports WithJsonHealthReport(this MetricsEndpointReports reports, string endpoint, bool alwaysReturnOkStatusCode = false) { - return reports.WithEndpointReport(endpoint, (d, h, r) => GetHealthResponse(h, alwaysReturnOkStatusCode)); + return reports.WithEndpointReport(endpoint, (d, h, r) => GetHealthResponse(h, GetJsonHealthCreator(r), GetJsonHealthMimeType(r), alwaysReturnOkStatusCode)); } - private static MetricsEndpointResponse GetHealthResponse(Func healthStatus, bool alwaysReturnOkStatusCode) + private static MetricsEndpointResponse GetHealthResponse(Func healthStatus, Func jsonCreator, string healthMimeType, bool alwaysReturnOkStatusCode) { var status = healthStatus(); - var json = JsonHealthChecks.BuildJson(status); + var json = jsonCreator(status); var httpStatus = status.IsHealthy || alwaysReturnOkStatusCode ? 200 : 500; var httpStatusDescription = status.IsHealthy || alwaysReturnOkStatusCode ? "OK" : "Internal Server Error"; - return new MetricsEndpointResponse(json, JsonHealthChecks.HealthChecksMimeType, Encoding.UTF8, httpStatus, httpStatusDescription); + return new MetricsEndpointResponse(json, healthMimeType, Encoding.UTF8, httpStatus, httpStatusDescription); } + private static Func GetJsonHealthCreator(MetricsEndpointRequest request) + { + return IsJsonHealthV2(request) + ? (Func)JsonHealthChecksV2.BuildJson + : JsonHealthChecksV1.BuildJson; + } + + private static string GetJsonHealthMimeType(MetricsEndpointRequest request) + { + return IsJsonHealthV2(request) + ? JsonHealthChecksV2.HealthChecksMimeType + : JsonHealthChecksV1.HealthChecksMimeType; + } + + private static bool IsJsonHealthV2(MetricsEndpointRequest request) + { + string[] acceptHeader; + if (request.Headers.TryGetValue("Accept", out acceptHeader)) + { + return acceptHeader.Contains(JsonHealthChecksV2.HealthChecksMimeType); + } + return false; + } + + #endregion HealthChecks + + #region MetricsData + public static MetricsEndpointReports WithJsonV1Report(this MetricsEndpointReports reports, string endpoint) { return reports.WithEndpointReport(endpoint, GetJsonV1Response); @@ -58,11 +98,6 @@ public static MetricsEndpointReports WithJsonReport(this MetricsEndpointReports return reports.WithEndpointReport(endpoint, GetJsonResponse); } - public static MetricsEndpointReports WithPing(this MetricsEndpointReports reports) - { - return reports.WithEndpointReport("/ping", (d, h, r) => new MetricsEndpointResponse("pong", "text/plain")); - } - private static MetricsEndpointResponse GetJsonResponse(MetricsData data, Func healthStatus, MetricsEndpointRequest request) { string[] acceptHeader; @@ -75,5 +110,12 @@ private static MetricsEndpointResponse GetJsonResponse(MetricsData data, Func new MetricsEndpointResponse("pong", "text/plain")); + } } } diff --git a/Src/Metrics/Reporters/MetricsEndpointReports.cs b/Src/Metrics/Reporters/MetricsEndpointReports.cs index 73de2df7..147ba0a7 100644 --- a/Src/Metrics/Reporters/MetricsEndpointReports.cs +++ b/Src/Metrics/Reporters/MetricsEndpointReports.cs @@ -39,8 +39,9 @@ private void RegisterDefaultEndpoints() { this .WithTextReport("/text") + .WithJsonHealthV1Report("/v1/health") + .WithJsonHealthV2Report("/v2/health") .WithJsonHealthReport("/health") - .WithJsonHealthReport("/v1/health") .WithJsonV1Report("/v1/json") .WithJsonV2Report("/v2/json") .WithJsonReport("/json")