diff --git a/Wave/Controllers/WebhookController.cs b/Wave/Controllers/WebhookController.cs index d429fcf..67b4cc3 100644 --- a/Wave/Controllers/WebhookController.cs +++ b/Wave/Controllers/WebhookController.cs @@ -4,16 +4,19 @@ using Microsoft.EntityFrameworkCore; using Wave.Data; using Wave.Data.Api.Mailtrap; +using Wave.Utilities.Metrics; namespace Wave.Controllers; [ApiController] [Route("/api/[controller]")] -public class WebhookController(ILogger logger, ApplicationDbContext context) : ControllerBase { +public class WebhookController(ILogger logger, ApplicationDbContext context, ApiMetrics metrics) + : ControllerBase { [HttpPost("mailtrap/{apiKey}")] [Authorize("EmailApi", AuthenticationSchemes = "ApiKeyInRoute")] public async Task Mailtrap(Webhook webhook, string apiKey) { foreach (var webhookEvent in webhook.Events) { + metrics.WebhookEventReceived("Mailtrap", webhookEvent.Type.ToString()); var subscriber = await context.Set().FirstOrDefaultAsync(s => s.Email == webhookEvent.Email); logger.LogDebug("Received Webhook event {EventType} for {email}", @@ -24,6 +27,7 @@ public class WebhookController(ILogger logger, ApplicationDbC "Received webhook event from mailtrap of type {EventType}, " + "but failed to find subscriber with E-Mail {email}.", webhookEvent.Type, webhookEvent.Email); + metrics.WebhookEventError("Mailtrap", webhookEvent.Type.ToString(), "unknown email"); continue; } @@ -59,6 +63,7 @@ public class WebhookController(ILogger logger, ApplicationDbC case WebhookEventType.Click: default: logger.LogInformation("Received unsupported event {EventType} for {email}. Skipping.", webhookEvent.Type, webhookEvent.Email); + metrics.WebhookEventError("Mailtrap", webhookEvent.Type.ToString(), "unknown type"); return Ok(); } diff --git a/Wave/Program.cs b/Wave/Program.cs index a330303..49b4a5e 100644 --- a/Wave/Program.cs +++ b/Wave/Program.cs @@ -26,6 +26,7 @@ using Serilog; using Serilog.Events; using Serilog.Sinks.Grafana.Loki; +using Wave.Utilities.Metrics; #region Version Information @@ -275,7 +276,7 @@ #endregion -#region Open Telemetry +#region Open Telemetry & Metrics var features = builder.Configuration.GetSection(nameof(Features)).Get(); if (features?.Telemetry is true) { @@ -292,7 +293,7 @@ .AddMeter("Microsoft.AspNetCore.Http.Connections") .AddMeter("Microsoft.AspNetCore.Http.Routing") .AddMeter("Microsoft.AspNetCore.Diagnostics") - + .AddMeter("Wave.Api") .AddPrometheusExporter()); // Jaeger etc. @@ -303,6 +304,8 @@ tracing.AddOtlpExporter(options => options.Endpoint = new Uri(otlpUrl)); }); } + + builder.Services.AddSingleton(); } #endregion diff --git a/Wave/Utilities/Metrics/ApiMetrics.cs b/Wave/Utilities/Metrics/ApiMetrics.cs new file mode 100644 index 0000000..c28e1d1 --- /dev/null +++ b/Wave/Utilities/Metrics/ApiMetrics.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.Metrics; + +namespace Wave.Utilities.Metrics; + +public class ApiMetrics { + private Counter WebhookEventCounter { get; } + private Counter WebhookErrorCounter { get; } + + public ApiMetrics(IMeterFactory meterFactory) { + var meter = meterFactory.Create("Wave.Api"); + + WebhookEventCounter = meter.CreateCounter("wave.webhook.events", "{events}", + description: "Counts the incoming webhook events"); + WebhookErrorCounter = meter.CreateCounter("wave.webhook.errors", "{events}", + description: "Counts errors in webhook events"); + } + + public void WebhookEventReceived(string api, string type) { + WebhookEventCounter.Add(1, + new KeyValuePair("wave.webhook.event_type", type), + new KeyValuePair("wave.webhook.api", api)); + } + + public void WebhookEventError(string api, string type, string reason) { + WebhookErrorCounter.Add(1, + new KeyValuePair("wave.webhook.event_type", type), + new KeyValuePair("wave.webhook.api", api), + new KeyValuePair("wave.error.reason", reason)); + } +} \ No newline at end of file