This commit is contained in:
Flaminel
2025-05-27 03:03:06 +03:00
parent a660480a7c
commit 0da1ef518a
16 changed files with 306 additions and 320 deletions

View File

@@ -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);
});
}
}

View File

@@ -0,0 +1,10 @@
namespace Data.Enums;
public enum EventSeverity
{
Test,
Information,
Warning,
Important,
Error,
}

View File

@@ -0,0 +1,11 @@
namespace Data.Enums;
public enum EventType
{
FailedImportStrike,
StalledStrike,
SlowStrike,
QueueItemDeleted,
DownloadCleaned,
CategoryChanged
}

View 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
}
}
}

View 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");
}
}
}

View 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
}
}
}

View File

@@ -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; }
}

View File

@@ -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);
}
}

View File

@@ -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"
}
});
});

View File

@@ -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)
{

View File

@@ -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
}
}
}

View File

@@ -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");
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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) {