Skip to content
Merged
6 changes: 6 additions & 0 deletions sama/Controllers/SettingsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public IActionResult Index()
SqlServerTable = _settingsService.Notifications_SqlServer_TableName,
SqlServerCreateTableScript = SqlServerNotificationService.CREATE_TABLE_SCRIPT.Trim(),

EventGridAccessKey = _settingsService.Notifications_EventGrid_AccessKey,
EventGridTopicEndpoint = _settingsService.Notifications_EventGrid_TopicEndpoint,

LdapEnable = _settingsService.Ldap_Enable,
LdapHost = _settingsService.Ldap_Host,
LdapPort = _settingsService.Ldap_Port,
Expand Down Expand Up @@ -92,6 +95,9 @@ public IActionResult Index(SettingsViewModel vm)
_settingsService.Notifications_SqlServer_Connection = vm.SqlServerConnection;
_settingsService.Notifications_SqlServer_TableName = vm.SqlServerTable;

_settingsService.Notifications_EventGrid_AccessKey = vm.EventGridAccessKey;
_settingsService.Notifications_EventGrid_TopicEndpoint = vm.EventGridTopicEndpoint;

_settingsService.Ldap_Enable = vm.LdapEnable;
_settingsService.Ldap_Host = vm.LdapHost;
_settingsService.Ldap_Port = vm.LdapPort;
Expand Down
5 changes: 5 additions & 0 deletions sama/Models/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public class SettingsViewModel

public string? SqlServerCreateTableScript { get; set; }

[Display(Name = "Event Grid Access Key")]
public string? EventGridAccessKey { get; set; }
[Display(Name = "Event Grid Topic Endpoint URL")]
public string? EventGridTopicEndpoint { get; set; }


[Display(Name = "Enable LDAP")]
public bool LdapEnable { get; set; }
Expand Down
159 changes: 159 additions & 0 deletions sama/Services/EventGridNotificationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using Azure.Messaging.EventGrid;
using Microsoft.Extensions.Logging;
using sama.Models;
using System;
using System.Threading.Tasks;

namespace sama.Services;

public class EventGridNotificationService : INotificationService
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: spacing was not reformatted after switching namespace style

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will def fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushed an update.

{
private readonly ILogger<EventGridNotificationService> _logger;
private readonly SettingsService _settings;
private readonly BackgroundExecutionWrapper _bgExec;
private readonly EventGridPublisherClientWrapper _eventGridWrapper;

public EventGridNotificationService(ILogger<EventGridNotificationService> logger, SettingsService settings, BackgroundExecutionWrapper bgExec, EventGridPublisherClientWrapper eventGridWrapper)
{
_logger = logger;
_settings = settings;
_bgExec = bgExec;
_eventGridWrapper = eventGridWrapper;
}

private static class EventTypes
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a class here? It just contains const strings, so couldn't they exist in the parent class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to collect them all together. just how my brain wanted to organize them. no pref either way for me.

{
public const string CheckCompleted = "sama.endpoint.check.completed";
public const string StatusUp = "sama.endpoint.status.up";
public const string StatusDown = "sama.endpoint.status.down";
public const string ManagementAdded = "sama.endpoint.management.added";
public const string ManagementRemoved = "sama.endpoint.management.removed";
public const string ManagementEnabled = "sama.endpoint.management.enabled";
public const string ManagementDisabled = "sama.endpoint.management.disabled";
public const string ManagementReconfigured = "sama.endpoint.management.reconfigured";
public const string ManagementUnknown = "sama.endpoint.management.unknown";
}

private static string Subject(string path) => $"Sama/{path}";
private static string EndpointSubject(string path) => Subject($"endpoints/{path}");

public virtual void NotifySingleResult(Endpoint endpoint, EndpointCheckResult result)
{
SendEvent(
EventTypes.CheckCompleted,
EndpointSubject($"{endpoint.Id}"),
new
{
endpointId = endpoint.Id,
endpointName = endpoint.Name,
success = result.Success,
responseTime = result.ResponseTime?.TotalMilliseconds,
startTime = result.Start,
stopTime = result.Stop,
error = result.Error?.Message
}
);
}

public virtual void NotifyUp(Endpoint endpoint, DateTimeOffset? downAsOf)
{
var downtimeMinutes = downAsOf.HasValue
? (int)DateTimeOffset.UtcNow.Subtract(downAsOf.Value).TotalMinutes
: 0;

SendEvent(
EventTypes.StatusUp,
EndpointSubject($"{endpoint.Id}"),
new
{
endpointId = endpoint.Id,
endpointName = endpoint.Name,
downAsOf = downAsOf,
downtimeMinutes = downtimeMinutes,
recoveredAt = DateTimeOffset.UtcNow
}
);
}

public virtual void NotifyDown(Endpoint endpoint, DateTimeOffset downAsOf, Exception? reason)
{
SendEvent(
EventTypes.StatusDown,
EndpointSubject($"{endpoint.Id}"),
new
{
endpointId = endpoint.Id,
endpointName = endpoint.Name,
downAsOf = downAsOf,
reason = reason?.Message,
reasonType = reason?.GetType().Name
}
);
}

public virtual void NotifyMisc(Endpoint endpoint, NotificationType type)
{
var eventType = type switch
{
NotificationType.EndpointAdded => EventTypes.ManagementAdded,
NotificationType.EndpointRemoved => EventTypes.ManagementRemoved,
NotificationType.EndpointEnabled => EventTypes.ManagementEnabled,
NotificationType.EndpointDisabled => EventTypes.ManagementDisabled,
NotificationType.EndpointReconfigured => EventTypes.ManagementReconfigured,
_ => EventTypes.ManagementUnknown
};

SendEvent(
eventType,
EndpointSubject($"{endpoint.Id}"),
new
{
endpointId = endpoint.Id,
endpointName = endpoint.Name,
notificationType = type.ToString(),
timestamp = DateTimeOffset.UtcNow
}
);
}

private async Task SendEventAsync(string eventType, string subject, object data)
{
try
{
if (!IsConfigured())
{
_logger.LogDebug("Event Grid notification service is not configured, skipping event");
return;
}

var topicEndpoint = new Uri(_settings.Notifications_EventGrid_TopicEndpoint!);
var accessKey = _settings.Notifications_EventGrid_AccessKey!;

var eventGridEvent = new EventGridEvent(
subject: subject,
eventType: eventType,
dataVersion: "1.0",
data: BinaryData.FromObjectAsJson(data)
);

await _eventGridWrapper.SendEventAsync(topicEndpoint, accessKey, eventGridEvent);

_logger.LogDebug("Successfully sent Event Grid event: {EventType} for {Subject}", eventType, subject);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send Event Grid notification for event type {EventType} and subject {Subject}", eventType, subject);
}
}

private void SendEvent(string eventType, string subject, object data)
{
_bgExec.Execute(() => SendEventAsync(eventType, subject, data).Wait());
}

private bool IsConfigured()
{
return !string.IsNullOrWhiteSpace(_settings.Notifications_EventGrid_TopicEndpoint)
&& !string.IsNullOrWhiteSpace(_settings.Notifications_EventGrid_AccessKey);
}
}
32 changes: 32 additions & 0 deletions sama/Services/EventGridPublisherClientWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Azure;
using Azure.Messaging.EventGrid;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace sama.Services;

/// <summary>
/// This is a wrapper around EventGridPublisherClient functionality that cannot be (easily) tested.
/// </summary>
[ExcludeFromCodeCoverage]
public class EventGridPublisherClientWrapper
{
public virtual async Task SendEventAsync(Uri topicEndpoint, string accessKey, EventGridEvent eventGridEvent)
{
if (topicEndpoint == null)
{
throw new ArgumentNullException(nameof(topicEndpoint), "Topic endpoint is not configured");
}

if (string.IsNullOrWhiteSpace(accessKey))
{
throw new ArgumentNullException(nameof(accessKey), "Access key is not configured");
}

var credential = new AzureKeyCredential(accessKey);
var client = new EventGridPublisherClient(topicEndpoint, credential);

await client.SendEventAsync(eventGridEvent);
}
}
12 changes: 12 additions & 0 deletions sama/Services/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ public virtual string? Notifications_SqlServer_TableName
set { SetSetting("SqlServerNotifications", "TableName", value); }
}

public virtual string? Notifications_EventGrid_TopicEndpoint
{
get { return GetSetting("EventGridNotifications", "TopicEndpoint", ""); }
set { SetSetting("EventGridNotifications", "TopicEndpoint", value); }
}

public virtual string? Notifications_EventGrid_AccessKey
{
get { return GetSetting("EventGridNotifications", "AccessKey", ""); }
set { SetSetting("EventGridNotifications", "AccessKey", value); }
}

public virtual int Monitor_IntervalSeconds
{
get { return GetSetting("Monitor", "IntervalSeconds", 90); }
Expand Down
2 changes: 2 additions & 0 deletions sama/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<PingWrapper>();
services.AddSingleton<TcpClientWrapper>();
services.AddSingleton<SqlConnectionWrapper>();
services.AddSingleton<EventGridPublisherClientWrapper>();
services.AddSingleton<CertificateValidationService>();
services.AddSingleton<BackgroundExecutionWrapper>();

Expand All @@ -80,6 +81,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<INotificationService, SlackNotificationService>();
services.AddSingleton<INotificationService, GraphiteNotificationService>();
services.AddSingleton<INotificationService, SqlServerNotificationService>();
services.AddSingleton<INotificationService, EventGridNotificationService>();
services.AddSingleton<AggregateNotificationService>();

services.AddSingleton<HttpHandlerFactory>();
Expand Down
15 changes: 15 additions & 0 deletions sama/Views/Settings/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@
<span asp-validation-for="SqlServerTable" class="text-danger"></span>
<p class="text-left">Example: <code>dbo.SamaEvents</code></p>
</div>

<h4>Azure Event Grid</h4>
<p class="text-left">All events can be sent to Azure Event Grid. The configured topic will receive all events.</p>
<div class="form-group">
<label asp-for="EventGridAccessKey" class="control-label"></label>
<input asp-for="EventGridAccessKey" class="form-control" />
<span asp-validation-for="EventGridAccessKey" class="text-danger"></span>
<p class="text-left">Configured in Azure Portal in your event grid topic under "Access Keys".</p>
</div>
<div class="form-group">
<label asp-for="EventGridTopicEndpoint" class="control-label"></label>
<input asp-for="EventGridTopicEndpoint" class="form-control" />
<span asp-validation-for="EventGridTopicEndpoint" class="text-danger"></span>
<p class="text-left">Example: <code>https://your-topic-name.eastus-1.eventgrid.azure.net/api/events</code></p>
</div>
</div>
<div class="col-xs-12 col-sm-4">
<h3>LDAP</h3>
Expand Down
1 change: 1 addition & 0 deletions sama/sama.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Messaging.EventGrid" Version="5.0.0" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="FluentScheduler" Version="5.5.1" />
<PackageReference Include="Humanizer" Version="2.14.1" />
Expand Down
Loading
Loading