mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-06-22 12:39:37 -04:00
events #5
This commit is contained in:
@@ -19,21 +19,4 @@ public class DataContext : DbContext
|
||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Additional configuration if needed
|
||||
modelBuilder.Entity<AppEvent>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Timestamp).IsRequired();
|
||||
entity.Property(e => e.EventType).IsRequired().HasMaxLength(100);
|
||||
entity.Property(e => e.Source).IsRequired().HasMaxLength(100);
|
||||
entity.Property(e => e.Message).IsRequired().HasMaxLength(1000);
|
||||
entity.Property(e => e.Severity).IsRequired().HasMaxLength(20);
|
||||
entity.Property(e => e.CorrelationId).HasMaxLength(50);
|
||||
});
|
||||
}
|
||||
}
|
||||
10
code/Data/Enums/EventSeverity.cs
Normal file
10
code/Data/Enums/EventSeverity.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Data.Enums;
|
||||
|
||||
public enum EventSeverity
|
||||
{
|
||||
Test,
|
||||
Information,
|
||||
Warning,
|
||||
Important,
|
||||
Error,
|
||||
}
|
||||
11
code/Data/Enums/EventType.cs
Normal file
11
code/Data/Enums/EventType.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Data.Enums;
|
||||
|
||||
public enum EventType
|
||||
{
|
||||
FailedImportStrike,
|
||||
StalledStrike,
|
||||
SlowStrike,
|
||||
QueueItemDeleted,
|
||||
DownloadCleaned,
|
||||
CategoryChanged
|
||||
}
|
||||
65
code/Data/Migrations/20250526234610_Initial.Designer.cs
generated
Normal file
65
code/Data/Migrations/20250526234610_Initial.Designer.cs
generated
Normal file
@@ -0,0 +1,65 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20250526234610_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.5");
|
||||
|
||||
modelBuilder.Entity("Data.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventType");
|
||||
|
||||
b.HasIndex("Message");
|
||||
|
||||
b.HasIndex("Severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending();
|
||||
|
||||
b.ToTable("Events");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
60
code/Data/Migrations/20250526234610_Initial.cs
Normal file
60
code/Data/Migrations/20250526234610_Initial.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Events",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Timestamp = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
EventType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Message = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Data = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Severity = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TrackingId = table.Column<Guid>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Events", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Events_EventType",
|
||||
table: "Events",
|
||||
column: "EventType");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Events_Message",
|
||||
table: "Events",
|
||||
column: "Message");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Events_Severity",
|
||||
table: "Events",
|
||||
column: "Severity");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Events_Timestamp",
|
||||
table: "Events",
|
||||
column: "Timestamp",
|
||||
descending: new bool[0]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Events");
|
||||
}
|
||||
}
|
||||
}
|
||||
62
code/Data/Migrations/DataContextModelSnapshot.cs
Normal file
62
code/Data/Migrations/DataContextModelSnapshot.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
partial class DataContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.5");
|
||||
|
||||
modelBuilder.Entity("Data.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventType");
|
||||
|
||||
b.HasIndex("Message");
|
||||
|
||||
b.HasIndex("Severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending();
|
||||
|
||||
b.ToTable("Events");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Data.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Data.Models.Events;
|
||||
@@ -6,25 +7,20 @@ namespace Data.Models.Events;
|
||||
/// <summary>
|
||||
/// Represents an event in the system
|
||||
/// </summary>
|
||||
[Index(nameof(Timestamp), IsDescending = new[] { true })]
|
||||
[Index(nameof(Timestamp), IsDescending = [true])]
|
||||
[Index(nameof(EventType))]
|
||||
[Index(nameof(Severity))]
|
||||
[Index(nameof(Source))]
|
||||
[Index(nameof(Message))]
|
||||
public class AppEvent
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||
|
||||
[Required]
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string EventType { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string Source { get; set; } = string.Empty;
|
||||
public EventType EventType { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(1000)]
|
||||
@@ -36,12 +32,10 @@ public class AppEvent
|
||||
public string? Data { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(20)]
|
||||
public string Severity { get; set; } = "Info"; // Info, Warning, Error, Critical
|
||||
public required EventSeverity Severity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional correlation ID to link related events
|
||||
/// </summary>
|
||||
[MaxLength(50)]
|
||||
public string? CorrelationId { get; set; }
|
||||
public Guid? TrackingId { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Data;
|
||||
using Data.Models.Events;
|
||||
using Data.Enums;
|
||||
using Infrastructure.Events;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -24,20 +25,22 @@ public class EventsController : ControllerBase
|
||||
public async Task<ActionResult<List<AppEvent>>> GetEvents(
|
||||
[FromQuery] int count = 100,
|
||||
[FromQuery] string? severity = null,
|
||||
[FromQuery] string? eventType = null,
|
||||
[FromQuery] string? source = null)
|
||||
[FromQuery] string? eventType = null)
|
||||
{
|
||||
var query = _context.Events.AsQueryable();
|
||||
|
||||
// Apply filters
|
||||
if (!string.IsNullOrWhiteSpace(severity))
|
||||
query = query.Where(e => e.Severity == severity);
|
||||
{
|
||||
if (Enum.TryParse<EventSeverity>(severity, true, out var severityEnum))
|
||||
query = query.Where(e => e.Severity == severityEnum);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(eventType))
|
||||
query = query.Where(e => e.EventType == eventType);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(source))
|
||||
query = query.Where(e => e.Source.Contains(source));
|
||||
{
|
||||
if (Enum.TryParse<EventType>(eventType, true, out var eventTypeEnum))
|
||||
query = query.Where(e => e.EventType == eventTypeEnum);
|
||||
}
|
||||
|
||||
// Order and limit
|
||||
var events = await query
|
||||
@@ -52,7 +55,7 @@ public class EventsController : ControllerBase
|
||||
/// Gets a specific event by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<AppEvent>> GetEvent(string id)
|
||||
public async Task<ActionResult<AppEvent>> GetEvent(Guid id)
|
||||
{
|
||||
var eventEntity = await _context.Events.FindAsync(id);
|
||||
|
||||
@@ -63,13 +66,13 @@ public class EventsController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets events by correlation ID
|
||||
/// Gets events by tracking ID
|
||||
/// </summary>
|
||||
[HttpGet("correlation/{correlationId}")]
|
||||
public async Task<ActionResult<List<AppEvent>>> GetEventsByCorrelation(string correlationId)
|
||||
[HttpGet("tracking/{trackingId}")]
|
||||
public async Task<ActionResult<List<AppEvent>>> GetEventsByTracking(Guid trackingId)
|
||||
{
|
||||
var events = await _context.Events
|
||||
.Where(e => e.CorrelationId == correlationId)
|
||||
.Where(e => e.TrackingId == trackingId)
|
||||
.OrderBy(e => e.Timestamp)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -87,11 +90,11 @@ public class EventsController : ControllerBase
|
||||
TotalEvents = await _context.Events.CountAsync(),
|
||||
EventsBySeverity = await _context.Events
|
||||
.GroupBy(e => e.Severity)
|
||||
.Select(g => new { Severity = g.Key, Count = g.Count() })
|
||||
.Select(g => new { Severity = g.Key.ToString(), Count = g.Count() })
|
||||
.ToListAsync(),
|
||||
EventsByType = await _context.Events
|
||||
.GroupBy(e => e.EventType)
|
||||
.Select(g => new { EventType = g.Key, Count = g.Count() })
|
||||
.Select(g => new { EventType = g.Key.ToString(), Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(10)
|
||||
.ToListAsync(),
|
||||
@@ -118,33 +121,23 @@ public class EventsController : ControllerBase
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets unique event sources
|
||||
/// </summary>
|
||||
[HttpGet("sources")]
|
||||
public async Task<ActionResult<List<string>>> GetEventSources()
|
||||
{
|
||||
var sources = await _context.Events
|
||||
.Select(e => e.Source)
|
||||
.Distinct()
|
||||
.OrderBy(s => s)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(sources);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets unique event types
|
||||
/// </summary>
|
||||
[HttpGet("types")]
|
||||
public async Task<ActionResult<List<string>>> GetEventTypes()
|
||||
{
|
||||
var types = await _context.Events
|
||||
.Select(e => e.EventType)
|
||||
.Distinct()
|
||||
.OrderBy(t => t)
|
||||
.ToListAsync();
|
||||
|
||||
var types = Enum.GetNames(typeof(EventType)).ToList();
|
||||
return Ok(types);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets unique severities
|
||||
/// </summary>
|
||||
[HttpGet("severities")]
|
||||
public async Task<ActionResult<List<string>>> GetSeverities()
|
||||
{
|
||||
var severities = Enum.GetNames(typeof(EventSeverity)).ToList();
|
||||
return Ok(severities);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Infrastructure.Health;
|
||||
using Infrastructure.Logging;
|
||||
using Infrastructure.Events;
|
||||
@@ -10,11 +11,21 @@ public static class ApiDI
|
||||
public static IServiceCollection AddApiServices(this IServiceCollection services)
|
||||
{
|
||||
// Add API-specific services
|
||||
services.AddControllers();
|
||||
services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
services.AddEndpointsApiExplorer();
|
||||
|
||||
// Add SignalR for real-time updates
|
||||
services.AddSignalR();
|
||||
services
|
||||
.AddSignalR()
|
||||
.AddJsonProtocol(options =>
|
||||
{
|
||||
options.PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});;
|
||||
|
||||
// Add health status broadcaster
|
||||
services.AddHostedService<HealthStatusBroadcaster>();
|
||||
@@ -26,12 +37,12 @@ public static class ApiDI
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "Cleanuperr API",
|
||||
Title = "Cleanuparr API",
|
||||
Version = "v1",
|
||||
Description = "API for managing media downloads and cleanups",
|
||||
Contact = new OpenApiContact
|
||||
{
|
||||
Name = "Cleanuperr Team"
|
||||
Name = "Cleanuparr Team"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
using Data;
|
||||
using Data.Enums;
|
||||
using Data.Models.Events;
|
||||
|
||||
namespace Infrastructure.Events;
|
||||
@@ -28,16 +29,15 @@ public class EventPublisher
|
||||
/// <summary>
|
||||
/// Publishes an event to database and SignalR clients
|
||||
/// </summary>
|
||||
public async Task PublishAsync(string eventType, string source, string message, string severity = "Info", object? data = null, string? correlationId = null)
|
||||
public async Task PublishAsync(EventType eventType, string message, EventSeverity severity, object? data = null, Guid? trackingId = null)
|
||||
{
|
||||
var eventEntity = new AppEvent
|
||||
{
|
||||
EventType = eventType,
|
||||
Source = source,
|
||||
Message = message,
|
||||
Severity = severity,
|
||||
Data = data != null ? JsonSerializer.Serialize(data) : null,
|
||||
CorrelationId = correlationId
|
||||
TrackingId = trackingId
|
||||
};
|
||||
|
||||
// Save to database
|
||||
@@ -47,55 +47,7 @@ public class EventPublisher
|
||||
// Send to SignalR clients
|
||||
await NotifyClientsAsync(eventEntity);
|
||||
|
||||
_logger.LogTrace("Published event: {eventType} from {source}", eventType, source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes an info event
|
||||
/// </summary>
|
||||
public async Task PublishInfoAsync(string source, string message, object? data = null, string? correlationId = null)
|
||||
{
|
||||
await PublishAsync("Information", source, message, "Info", data, correlationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a warning event
|
||||
/// </summary>
|
||||
public async Task PublishWarningAsync(string source, string message, object? data = null, string? correlationId = null)
|
||||
{
|
||||
await PublishAsync("Warning", source, message, "Warning", data, correlationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes an error event
|
||||
/// </summary>
|
||||
public async Task PublishErrorAsync(string source, string message, object? data = null, string? correlationId = null)
|
||||
{
|
||||
await PublishAsync("Error", source, message, "Error", data, correlationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a notification-related event (for HTTP notifications to Notifiarr/Apprise)
|
||||
/// </summary>
|
||||
public async Task PublishNotificationEventAsync(string provider, string message, bool success, object? data = null, string? correlationId = null)
|
||||
{
|
||||
var eventType = success ? "NotificationSent" : "NotificationFailed";
|
||||
var severity = success ? "Info" : "Warning";
|
||||
|
||||
await PublishAsync(eventType, $"NotificationService.{provider}", message, severity, data, correlationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes an HTTP call event (for external API calls)
|
||||
/// </summary>
|
||||
public async Task PublishHttpCallEventAsync(string endpoint, string method, int statusCode, TimeSpan duration, object? data = null, string? correlationId = null)
|
||||
{
|
||||
var success = statusCode >= 200 && statusCode < 300;
|
||||
var eventType = success ? "HttpCallSuccess" : "HttpCallFailed";
|
||||
var severity = success ? "Info" : "Warning";
|
||||
var message = $"{method} {endpoint} - {statusCode} ({duration.TotalMilliseconds}ms)";
|
||||
|
||||
await PublishAsync(eventType, "HttpClient", message, severity, data, correlationId);
|
||||
_logger.LogTrace("Published event: {eventType}", eventType);
|
||||
}
|
||||
|
||||
private async Task NotifyClientsAsync(AppEvent appEventEntity)
|
||||
@@ -104,8 +56,6 @@ public class EventPublisher
|
||||
{
|
||||
// Send to all connected clients (self-hosted app with single client)
|
||||
await _hubContext.Clients.All.SendAsync("EventReceived", appEventEntity);
|
||||
|
||||
_logger.LogTrace("Sent event {eventId} to SignalR clients", appEventEntity.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
using Infrastructure.Verticals.Notifications;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Infrastructure.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper around NotificationService that publishes events for all notification calls
|
||||
/// </summary>
|
||||
public class NotificationEventWrapper
|
||||
{
|
||||
private readonly NotificationService _notificationService;
|
||||
private readonly EventPublisher _eventPublisher;
|
||||
private readonly ILogger<NotificationEventWrapper> _logger;
|
||||
|
||||
public NotificationEventWrapper(
|
||||
NotificationService notificationService,
|
||||
EventPublisher eventPublisher,
|
||||
ILogger<NotificationEventWrapper> logger)
|
||||
{
|
||||
_notificationService = notificationService;
|
||||
_eventPublisher = eventPublisher;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Notify(FailedImportStrikeNotification notification)
|
||||
{
|
||||
await NotifyWithEventLogging("FailedImportStrike", notification,
|
||||
async () => await _notificationService.Notify(notification));
|
||||
}
|
||||
|
||||
public async Task Notify(StalledStrikeNotification notification)
|
||||
{
|
||||
await NotifyWithEventLogging("StalledStrike", notification,
|
||||
async () => await _notificationService.Notify(notification));
|
||||
}
|
||||
|
||||
public async Task Notify(SlowStrikeNotification notification)
|
||||
{
|
||||
await NotifyWithEventLogging("SlowStrike", notification,
|
||||
async () => await _notificationService.Notify(notification));
|
||||
}
|
||||
|
||||
public async Task Notify(QueueItemDeletedNotification notification)
|
||||
{
|
||||
await NotifyWithEventLogging("QueueItemDeleted", notification,
|
||||
async () => await _notificationService.Notify(notification));
|
||||
}
|
||||
|
||||
public async Task Notify(DownloadCleanedNotification notification)
|
||||
{
|
||||
await NotifyWithEventLogging("DownloadCleaned", notification,
|
||||
async () => await _notificationService.Notify(notification));
|
||||
}
|
||||
|
||||
public async Task Notify(CategoryChangedNotification notification)
|
||||
{
|
||||
await NotifyWithEventLogging("CategoryChanged", notification,
|
||||
async () => await _notificationService.Notify(notification));
|
||||
}
|
||||
|
||||
private async Task NotifyWithEventLogging<T>(string notificationType, T notification, Func<Task> notifyAction)
|
||||
where T : class
|
||||
{
|
||||
var correlationId = Guid.NewGuid().ToString("N")[..8];
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// Log notification attempt
|
||||
await _eventPublisher.PublishInfoAsync(
|
||||
source: "NotificationService",
|
||||
message: $"Sending {notificationType} notification",
|
||||
data: new { NotificationType = notificationType, Notification = notification },
|
||||
correlationId: correlationId);
|
||||
|
||||
// Execute the notification
|
||||
await notifyAction();
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
// Log successful notification
|
||||
await _eventPublisher.PublishInfoAsync(
|
||||
source: "NotificationService",
|
||||
message: $"{notificationType} notification sent successfully",
|
||||
data: new {
|
||||
NotificationType = notificationType,
|
||||
Duration = stopwatch.ElapsedMilliseconds,
|
||||
Success = true
|
||||
},
|
||||
correlationId: correlationId);
|
||||
|
||||
_logger.LogInformation("Successfully sent {notificationType} notification in {duration}ms",
|
||||
notificationType, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
|
||||
// Log failed notification
|
||||
await _eventPublisher.PublishErrorAsync(
|
||||
source: "NotificationService",
|
||||
message: $"Failed to send {notificationType} notification: {ex.Message}",
|
||||
data: new {
|
||||
NotificationType = notificationType,
|
||||
Duration = stopwatch.ElapsedMilliseconds,
|
||||
Success = false,
|
||||
Error = ex.Message,
|
||||
StackTrace = ex.StackTrace
|
||||
},
|
||||
correlationId: correlationId);
|
||||
|
||||
_logger.LogError(ex, "Failed to send {notificationType} notification after {duration}ms",
|
||||
notificationType, stopwatch.ElapsedMilliseconds);
|
||||
|
||||
throw; // Re-throw to maintain original behavior
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Data.Enums;
|
||||
using Infrastructure.Events;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -23,10 +24,9 @@ public class LoggingInitializer : BackgroundService
|
||||
try
|
||||
{
|
||||
await _eventPublisher.PublishAsync(
|
||||
"strike",
|
||||
EventType.SlowStrike,
|
||||
"test",
|
||||
"Item '{item}' has been struck {1} times for reason '{stalled}'",
|
||||
severity: "Warning",
|
||||
EventSeverity.Important,
|
||||
data: new { Hash = "hash", Name = "name", StrikeCount = "1", Type = "stalled" });
|
||||
throw new Exception("test exception");
|
||||
}
|
||||
|
||||
@@ -50,10 +50,9 @@ public sealed class Striker : IStriker
|
||||
|
||||
await _notifier.NotifyStrike(strikeType, strikeCount);
|
||||
await _eventPublisher.PublishAsync(
|
||||
"strike",
|
||||
nameof(Striker),
|
||||
EventType.SlowStrike, // TODO
|
||||
$"Item '{itemName}' has been struck {strikeCount} times for reason '{strikeType}'",
|
||||
severity: "Warning",
|
||||
EventSeverity.Important,
|
||||
data: new { hash, itemName, strikeCount, strikeType });
|
||||
|
||||
_cache.Set(key, strikeCount, _cacheOptions);
|
||||
|
||||
@@ -2,11 +2,10 @@ export interface Event {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
eventType: string;
|
||||
source: string;
|
||||
message: string;
|
||||
data?: string;
|
||||
severity: string;
|
||||
correlationId?: string;
|
||||
trackingId?: string;
|
||||
}
|
||||
|
||||
export interface EventStats {
|
||||
@@ -19,7 +18,6 @@ export interface EventStats {
|
||||
export interface EventFilter {
|
||||
severity?: string;
|
||||
eventType?: string;
|
||||
source?: string;
|
||||
search?: string;
|
||||
count?: number;
|
||||
}
|
||||
@@ -88,18 +88,6 @@
|
||||
[disabled]="!isConnected()"
|
||||
>
|
||||
</p-select>
|
||||
|
||||
<!-- Source Filter -->
|
||||
<p-select
|
||||
[options]="sources()"
|
||||
[ngModel]="sourceFilter()"
|
||||
placeholder="Filter by source"
|
||||
[showClear]="true"
|
||||
(onChange)="onSourceFilterChange($event.value)"
|
||||
styleClass="source-dropdown fixed-width-dropdown"
|
||||
[disabled]="!isConnected()"
|
||||
>
|
||||
</p-select>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="end">
|
||||
@@ -117,7 +105,7 @@
|
||||
label="Clear Filters"
|
||||
class="p-button-outlined ml-2 clear-filters-btn"
|
||||
(click)="clearFilters()"
|
||||
[disabled]="!isConnected() || (!severityFilter() && !eventTypeFilter() && !sourceFilter() && !searchFilter())"
|
||||
[disabled]="!isConnected() || (!severityFilter() && !eventTypeFilter() && !searchFilter())"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,12 +123,12 @@
|
||||
[ngClass]="getSeverityClass(event.severity)"
|
||||
[id]="'event-' + i"
|
||||
>
|
||||
<!-- Event Entry Header - only expandable if has data or correlation ID -->
|
||||
<!-- Event Entry Header - only expandable if has data or tracking ID -->
|
||||
<div
|
||||
class="event-entry-header"
|
||||
[class.expandable]="event.data || event.correlationId"
|
||||
[class.expandable]="event.data || event.trackingId"
|
||||
(click)="
|
||||
event.data || event.correlationId ? toggleEventExpansion(i, $event) : null
|
||||
event.data || event.trackingId ? toggleEventExpansion(i, $event) : null
|
||||
"
|
||||
>
|
||||
<!-- Actions (Copy button at start) -->
|
||||
@@ -170,29 +158,24 @@
|
||||
<span class="event-type-badge">{{ event.eventType }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Source -->
|
||||
<div class="event-source">
|
||||
<span class="event-source-badge">{{ event.source }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Message -->
|
||||
<div class="event-message">
|
||||
{{ event.message }}
|
||||
</div>
|
||||
|
||||
<!-- Correlation ID (if exists) -->
|
||||
<div class="event-correlation" *ngIf="event.correlationId">
|
||||
<!-- Tracking ID (if exists) -->
|
||||
<div class="event-tracking" *ngIf="event.trackingId">
|
||||
<p-tag
|
||||
[value]="event.correlationId"
|
||||
[value]="event.trackingId"
|
||||
severity="secondary"
|
||||
[rounded]="true"
|
||||
styleClass="correlation-tag"
|
||||
[pTooltip]="'Correlation ID: ' + event.correlationId"
|
||||
styleClass="tracking-tag"
|
||||
[pTooltip]="'Tracking ID: ' + event.trackingId"
|
||||
></p-tag>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown button (only for events with data or correlation ID) -->
|
||||
<div class="event-actions" *ngIf="event.data || event.correlationId">
|
||||
<!-- Dropdown button (only for events with data or tracking ID) -->
|
||||
<div class="event-actions" *ngIf="event.data || event.trackingId">
|
||||
<button
|
||||
pButton
|
||||
[icon]="expandedEvents[i] ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||
@@ -212,11 +195,11 @@
|
||||
<div class="data-content" *ngIf="!isValidJson(event.data)">{{ event.data }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Correlation Information -->
|
||||
<div class="event-metadata" *ngIf="event.correlationId">
|
||||
<!-- Tracking Information -->
|
||||
<div class="event-metadata" *ngIf="event.trackingId">
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Correlation ID:</span>
|
||||
<span class="metadata-value">{{ event.correlationId }}</span>
|
||||
<span class="metadata-label">Tracking ID:</span>
|
||||
<span class="metadata-value">{{ event.trackingId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,6 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
// Filter state
|
||||
severityFilter = signal<string | null>(null);
|
||||
eventTypeFilter = signal<string | null>(null);
|
||||
sourceFilter = signal<string | null>(null);
|
||||
searchFilter = signal<string>('');
|
||||
|
||||
// Export menu items
|
||||
@@ -87,18 +86,13 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
filtered = filtered.filter(event => event.eventType === this.eventTypeFilter());
|
||||
}
|
||||
|
||||
if (this.sourceFilter()) {
|
||||
filtered = filtered.filter(event => event.source.includes(this.sourceFilter()!));
|
||||
}
|
||||
|
||||
if (this.searchFilter()) {
|
||||
const search = this.searchFilter().toLowerCase();
|
||||
filtered = filtered.filter(event =>
|
||||
event.message.toLowerCase().includes(search) ||
|
||||
event.source.toLowerCase().includes(search) ||
|
||||
event.eventType.toLowerCase().includes(search) ||
|
||||
(event.data && event.data.toLowerCase().includes(search)) ||
|
||||
(event.correlationId && event.correlationId.toLowerCase().includes(search)));
|
||||
(event.trackingId && event.trackingId.toLowerCase().includes(search)));
|
||||
}
|
||||
|
||||
return filtered;
|
||||
@@ -114,11 +108,6 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
return uniqueTypes.map(type => ({ label: type, value: type }));
|
||||
});
|
||||
|
||||
sources = computed(() => {
|
||||
const uniqueSources = [...new Set(this.events().map(event => event.source))];
|
||||
return uniqueSources.map(source => ({ label: source, value: source }));
|
||||
});
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -174,10 +163,6 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
this.eventTypeFilter.set(eventType);
|
||||
}
|
||||
|
||||
onSourceFilterChange(source: string): void {
|
||||
this.sourceFilter.set(source);
|
||||
}
|
||||
|
||||
onSearchChange(event: Event): void {
|
||||
const searchText = (event.target as HTMLInputElement).value;
|
||||
this.search$.next(searchText);
|
||||
@@ -186,7 +171,6 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
clearFilters(): void {
|
||||
this.severityFilter.set(null);
|
||||
this.eventTypeFilter.set(null);
|
||||
this.sourceFilter.set(null);
|
||||
this.searchFilter.set('');
|
||||
}
|
||||
|
||||
@@ -195,13 +179,15 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
|
||||
switch (normalizedSeverity) {
|
||||
case 'error':
|
||||
case 'critical':
|
||||
return 'danger';
|
||||
case 'warning':
|
||||
return 'warn';
|
||||
case 'info':
|
||||
case 'information':
|
||||
return 'info';
|
||||
case 'important':
|
||||
return 'warn';
|
||||
case 'test':
|
||||
return 'secondary';
|
||||
default:
|
||||
return 'secondary';
|
||||
}
|
||||
@@ -215,8 +201,8 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
return this.events().some(event => event.data);
|
||||
}
|
||||
|
||||
hasCorrelationInfo(): boolean {
|
||||
return this.events().some(event => event.correlationId);
|
||||
hasTrackingInfo(): boolean {
|
||||
return this.events().some(event => event.trackingId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,13 +213,15 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
|
||||
switch (normalizedSeverity) {
|
||||
case 'error':
|
||||
case 'critical':
|
||||
return 'severity-error';
|
||||
case 'warning':
|
||||
return 'severity-warning';
|
||||
case 'info':
|
||||
case 'information':
|
||||
return 'severity-info';
|
||||
case 'important':
|
||||
return 'severity-warning';
|
||||
case 'test':
|
||||
return 'severity-default';
|
||||
default:
|
||||
return 'severity-default';
|
||||
}
|
||||
@@ -256,10 +244,10 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
domEvent.stopPropagation();
|
||||
|
||||
const timestamp = new Date(event.timestamp).toISOString();
|
||||
let content = `[${timestamp}] [${event.severity}] [${event.eventType}] [${event.source}] ${event.message}`;
|
||||
let content = `[${timestamp}] [${event.severity}] [${event.eventType}] ${event.message}`;
|
||||
|
||||
if (event.correlationId) {
|
||||
content += `\nCorrelation ID: ${event.correlationId}`;
|
||||
if (event.trackingId) {
|
||||
content += `\nTracking ID: ${event.trackingId}`;
|
||||
}
|
||||
|
||||
if (event.data) {
|
||||
@@ -278,10 +266,10 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
|
||||
const content = events.map(event => {
|
||||
const timestamp = new Date(event.timestamp).toISOString();
|
||||
let entry = `[${timestamp}] [${event.severity}] [${event.eventType}] [${event.source}] ${event.message}`;
|
||||
let entry = `[${timestamp}] [${event.severity}] [${event.eventType}] ${event.message}`;
|
||||
|
||||
if (event.correlationId) {
|
||||
entry += `\nCorrelation ID: ${event.correlationId}`;
|
||||
if (event.trackingId) {
|
||||
entry += `\nTracking ID: ${event.trackingId}`;
|
||||
}
|
||||
|
||||
if (event.data) {
|
||||
@@ -322,19 +310,18 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
if (events.length === 0) return;
|
||||
|
||||
// CSV header
|
||||
let csv = 'Timestamp,Severity,EventType,Source,Message,Data,CorrelationId\n';
|
||||
let csv = 'Timestamp,Severity,EventType,Message,Data,TrackingId\n';
|
||||
|
||||
// CSV rows
|
||||
events.forEach(event => {
|
||||
const timestamp = new Date(event.timestamp).toISOString();
|
||||
const severity = event.severity || '';
|
||||
const eventType = event.eventType ? `"${event.eventType.replace(/"/g, '""')}"` : '';
|
||||
const source = event.source ? `"${event.source.replace(/"/g, '""')}"` : '';
|
||||
const message = event.message ? `"${event.message.replace(/"/g, '""')}"` : '';
|
||||
const data = event.data ? `"${event.data.replace(/"/g, '""').replace(/\n/g, ' ')}"` : '';
|
||||
const correlationId = event.correlationId ? `"${event.correlationId.replace(/"/g, '""')}"` : '';
|
||||
const trackingId = event.trackingId ? `"${event.trackingId.replace(/"/g, '""')}"` : '';
|
||||
|
||||
csv += `${timestamp},${severity},${eventType},${source},${message},${data},${correlationId}\n`;
|
||||
csv += `${timestamp},${severity},${eventType},${message},${data},${trackingId}\n`;
|
||||
});
|
||||
|
||||
this.downloadFile(csv, 'text/csv', 'events.csv');
|
||||
@@ -349,10 +336,10 @@ export class EventsViewerComponent implements OnInit, OnDestroy {
|
||||
|
||||
const content = events.map(event => {
|
||||
const timestamp = new Date(event.timestamp).toISOString();
|
||||
let entry = `[${timestamp}] [${event.severity}] [${event.eventType}] [${event.source}] ${event.message}`;
|
||||
let entry = `[${timestamp}] [${event.severity}] [${event.eventType}] ${event.message}`;
|
||||
|
||||
if (event.correlationId) {
|
||||
entry += `\nCorrelation ID: ${event.correlationId}`;
|
||||
if (event.trackingId) {
|
||||
entry += `\nTracking ID: ${event.trackingId}`;
|
||||
}
|
||||
|
||||
if (event.data) {
|
||||
|
||||
Reference in New Issue
Block a user