From f9386294a1780d930c45eb22d9764979f1fb4148 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Tue, 23 Jan 2024 07:22:21 +0300 Subject: [PATCH] Added sample for health checks --- README.md | 1 + Tingle.EventBus.sln | 7 ++++ .../HealthCheck/AzureServiceBusHealthCheck.cs | 38 +++++++++++++++++++ samples/HealthCheck/HealthCheck.csproj | 12 ++++++ samples/HealthCheck/Program.cs | 33 ++++++++++++++++ .../Properties/launchSettings.json | 11 ++++++ samples/HealthCheck/VehicleDoorOpenedEvent.cs | 9 +++++ samples/HealthCheck/VehicleTelemetryEvent.cs | 35 +++++++++++++++++ .../VehicleTelemetryEventsConsumer.cs | 35 +++++++++++++++++ .../HealthCheck/appsettings.Development.json | 16 ++++++++ samples/HealthCheck/appsettings.json | 9 +++++ 11 files changed, 206 insertions(+) create mode 100644 samples/HealthCheck/AzureServiceBusHealthCheck.cs create mode 100644 samples/HealthCheck/HealthCheck.csproj create mode 100644 samples/HealthCheck/Program.cs create mode 100644 samples/HealthCheck/Properties/launchSettings.json create mode 100644 samples/HealthCheck/VehicleDoorOpenedEvent.cs create mode 100644 samples/HealthCheck/VehicleTelemetryEvent.cs create mode 100644 samples/HealthCheck/VehicleTelemetryEventsConsumer.cs create mode 100644 samples/HealthCheck/appsettings.Development.json create mode 100644 samples/HealthCheck/appsettings.json diff --git a/README.md b/README.md index c640c80c..ee345398 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ A number of the documents below are still a work in progress and will be added a - [Using Amazon SQS and SNS](./samples/AmazonSqsAndSns) - [Receive events from Azure IoT Hub](./samples/AzureIotHub) - [Using Azure Managed Identity instead of Connection Strings](./samples/AzureManagedIdentity) +- [Health Checks for Azure Service Bus with Managed Identity](./samples/HealthCheck) ## Issues & Comments diff --git a/Tingle.EventBus.sln b/Tingle.EventBus.sln index 5ea7ebc8..91f32443 100644 --- a/Tingle.EventBus.sln +++ b/Tingle.EventBus.sln @@ -64,6 +64,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomEventConfigurator", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomEventSerializer", "samples\CustomEventSerializer\CustomEventSerializer.csproj", "{2C55FABC-8C94-4104-BE05-42A477D2AD9E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthCheck", "samples\HealthCheck\HealthCheck.csproj", "{BF5FE712-123E-4826-A6AC-C87C86D14724}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InMemoryBackgroundProcessing", "samples\InMemoryBackgroundProcessing\InMemoryBackgroundProcessing.csproj", "{C9293277-90BA-4F1A-BEA7-85CE41103B8D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiEventsConsumer", "samples\MultiEventsConsumer\MultiEventsConsumer.csproj", "{E9D6F25A-0586-4409-A3EB-A72B6E89A71C}" @@ -176,6 +178,10 @@ Global {2C55FABC-8C94-4104-BE05-42A477D2AD9E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C55FABC-8C94-4104-BE05-42A477D2AD9E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C55FABC-8C94-4104-BE05-42A477D2AD9E}.Release|Any CPU.Build.0 = Release|Any CPU + {BF5FE712-123E-4826-A6AC-C87C86D14724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF5FE712-123E-4826-A6AC-C87C86D14724}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF5FE712-123E-4826-A6AC-C87C86D14724}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF5FE712-123E-4826-A6AC-C87C86D14724}.Release|Any CPU.Build.0 = Release|Any CPU {C9293277-90BA-4F1A-BEA7-85CE41103B8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C9293277-90BA-4F1A-BEA7-85CE41103B8D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C9293277-90BA-4F1A-BEA7-85CE41103B8D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -232,6 +238,7 @@ Global {8E115759-87CC-4F45-9679-A9EBBD59992B} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {8C0EE13F-701F-45EF-BADF-6B7A22AA6785} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {2C55FABC-8C94-4104-BE05-42A477D2AD9E} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} + {BF5FE712-123E-4826-A6AC-C87C86D14724} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {C9293277-90BA-4F1A-BEA7-85CE41103B8D} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {E9D6F25A-0586-4409-A3EB-A72B6E89A71C} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {B2043778-DDC9-4396-801C-442EFF0C7E73} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} diff --git a/samples/HealthCheck/AzureServiceBusHealthCheck.cs b/samples/HealthCheck/AzureServiceBusHealthCheck.cs new file mode 100644 index 00000000..2f3f8c3a --- /dev/null +++ b/samples/HealthCheck/AzureServiceBusHealthCheck.cs @@ -0,0 +1,38 @@ +using Azure.Messaging.ServiceBus.Administration; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using Tingle.EventBus.Transports.Azure.ServiceBus; + +namespace HealthCheck; + +internal class AzureServiceBusHealthCheck(IOptionsMonitor optionsMonitor) : IHealthCheck +{ + private const string QueueName = "health-check"; + + private readonly AzureServiceBusTransportOptions options = optionsMonitor?.Get(AzureServiceBusDefaults.Name) ?? throw new ArgumentNullException(nameof(optionsMonitor)); + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var cred = (AzureServiceBusTransportCredentials)options.Credentials.CurrentValue; + var managementClient = new ServiceBusAdministrationClient( + fullyQualifiedNamespace: cred.FullyQualifiedNamespace, + credential: cred.TokenCredential); + + _ = await managementClient.GetQueueRuntimePropertiesAsync(QueueName, cancellationToken); + + return HealthCheckResult.Healthy(); + } + catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) + { + // ignore long running calls + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + } +} + diff --git a/samples/HealthCheck/HealthCheck.csproj b/samples/HealthCheck/HealthCheck.csproj new file mode 100644 index 00000000..8b06280e --- /dev/null +++ b/samples/HealthCheck/HealthCheck.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/samples/HealthCheck/Program.cs b/samples/HealthCheck/Program.cs new file mode 100644 index 00000000..32535e37 --- /dev/null +++ b/samples/HealthCheck/Program.cs @@ -0,0 +1,33 @@ +using Azure.Identity; +using HealthCheck; +using Tingle.EventBus.Configuration; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + var configuration = hostContext.Configuration; + + services.AddEventBus(builder => + { + builder.AddConsumer(); + + var credential = new DefaultAzureCredential(); + + // Transport specific configuration + builder.AddAzureServiceBusTransport(options => + { + options.Credentials = new AzureServiceBusTransportCredentials + { + TokenCredential = credential, + FullyQualifiedNamespace = "{your_namespace}.servicebus.windows.net" + }; + options.DefaultEntityKind = EntityKind.Queue; // required if using the basic SKU (does not support topics) + }); + + builder.Services.AddHealthChecks() + .AddCheck(name: "servicebus", tags: ["eventbus"]); + }); + }) + .Build(); + +await host.RunAsync(); diff --git a/samples/HealthCheck/Properties/launchSettings.json b/samples/HealthCheck/Properties/launchSettings.json new file mode 100644 index 00000000..d3e9614a --- /dev/null +++ b/samples/HealthCheck/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HealthCheck": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/HealthCheck/VehicleDoorOpenedEvent.cs b/samples/HealthCheck/VehicleDoorOpenedEvent.cs new file mode 100644 index 00000000..cf6d1e98 --- /dev/null +++ b/samples/HealthCheck/VehicleDoorOpenedEvent.cs @@ -0,0 +1,9 @@ +namespace HealthCheck; + +public class VehicleDoorOpenedEvent +{ + public string? VehicleId { get; set; } + public VehicleDoorKind Kind { get; set; } + public DateTimeOffset? Opened { get; set; } + public DateTimeOffset? Closed { get; set; } +} diff --git a/samples/HealthCheck/VehicleTelemetryEvent.cs b/samples/HealthCheck/VehicleTelemetryEvent.cs new file mode 100644 index 00000000..a384f385 --- /dev/null +++ b/samples/HealthCheck/VehicleTelemetryEvent.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace HealthCheck; + +internal class VehicleTelemetryEvent +{ + public string? DeviceId { get; set; } + + public DateTimeOffset Timestamp { get; set; } + + public string? Action { get; set; } + + public VehicleDoorKind? VehicleDoorKind { get; set; } + public VehicleDoorStatus? VehicleDoorStatus { get; set; } + + [JsonExtensionData] + public Dictionary? Extras { get; set; } +} + +public enum VehicleDoorStatus +{ + Unknown, + Open, + Closed, +} + +public enum VehicleDoorKind +{ + FrontLeft, + FrontRight, + RearLeft, + ReadRight, + Hood, + Trunk, +} diff --git a/samples/HealthCheck/VehicleTelemetryEventsConsumer.cs b/samples/HealthCheck/VehicleTelemetryEventsConsumer.cs new file mode 100644 index 00000000..05788d1b --- /dev/null +++ b/samples/HealthCheck/VehicleTelemetryEventsConsumer.cs @@ -0,0 +1,35 @@ +namespace HealthCheck; + +internal class VehicleTelemetryEventsConsumer(ILogger logger) : IEventConsumer +{ + public async Task ConsumeAsync(EventContext context, CancellationToken cancellationToken) + { + var telemetry = context.Event; + + var status = telemetry.VehicleDoorStatus; + if (status is not VehicleDoorStatus.Open and not VehicleDoorStatus.Closed) + { + logger.LogWarning("Vehicle Door status '{VehicleDoorStatus}' is not yet supported", status); + return; + } + + var kind = telemetry.VehicleDoorKind; + if (kind is null) + { + logger.LogWarning("Vehicle Door kind '{VehicleDoorKind}' cannot be null", kind); + return; + } + + var timestamp = telemetry.Timestamp; + var updateEvt = new VehicleDoorOpenedEvent + { + VehicleId = telemetry.DeviceId, + Kind = kind.Value, + Closed = status is VehicleDoorStatus.Closed ? timestamp : null, + Opened = status is VehicleDoorStatus.Open ? timestamp : null, + }; + + // the VehicleDoorOpenedEvent on a broadcast bus would notify all subscribers + await context.PublishAsync(updateEvt, cancellationToken: cancellationToken); + } +} diff --git a/samples/HealthCheck/appsettings.Development.json b/samples/HealthCheck/appsettings.Development.json new file mode 100644 index 00000000..80698e75 --- /dev/null +++ b/samples/HealthCheck/appsettings.Development.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Information", + "System": "Information" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "SingleLine": true, + "TimestampFormat": "HH:mm:ss " + } + } + } +} diff --git a/samples/HealthCheck/appsettings.json b/samples/HealthCheck/appsettings.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/samples/HealthCheck/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}