@@ -39,7 +20,7 @@
- © 2026 - EonaCat.Logger.LogServer - Privacy + © @DateTime.Now.Year - EonaCat (Jeroen Saey)
diff --git a/EonaCat.Logger.LogServer/Program.cs b/EonaCat.Logger.LogServer/Program.cs index 1f190ec..d060f21 100644 --- a/EonaCat.Logger.LogServer/Program.cs +++ b/EonaCat.Logger.LogServer/Program.cs @@ -1,17 +1,18 @@ -using LogCentral.Server.Data; -using LogCentral.Server.Services; +using EonaCat.Logger.Server.Data; +using EonaCat.Logger.Server.Services; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddControllers(); -builder.Services.AddDbContext(options => +builder.Services.AddDbContext(options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddMemoryCache(); builder.Services.AddCors(options => { @@ -21,17 +22,11 @@ builder.Services.AddCors(options => }); }); -builder.Services.AddControllers() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; - }); - var app = builder.Build(); using (var scope = app.Services.CreateScope()) { - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); } diff --git a/EonaCat.Logger.LogServer/Services/IAnalyticsService.cs b/EonaCat.Logger.LogServer/Services/IAnalyticsService.cs index 2af7dbf..8ab3c73 100644 --- a/EonaCat.Logger.LogServer/Services/IAnalyticsService.cs +++ b/EonaCat.Logger.LogServer/Services/IAnalyticsService.cs @@ -1,37 +1,94 @@ -using LogCentral.Server.Data; -using LogCentral.Server.Services; +using EonaCat.Logger.Server.Data; +using EonaCat.Logger.Server.Services; using Microsoft.EntityFrameworkCore; public interface IAnalyticsService { - Task> GetAnalyticsAsync(DateTime startDate, DateTime endDate); + Task GetAnalyticsAsync(DateTime startDate, DateTime endDate); +} + +public class AnalyticsResult +{ + public int TotalEvents { get; set; } + public List TopEvents { get; set; } = new(); + public Dictionary EventsByApplication { get; set; } = new(); + public Dictionary EventsByHour { get; set; } = new(); + public List TopUsers { get; set; } = new(); +} + +public class EventSummary +{ + public string Event { get; set; } = string.Empty; + public int Count { get; set; } + public string? LastSeen { get; set; } +} + +public class UserActivity +{ + public string UserId { get; set; } = string.Empty; + public int EventCount { get; set; } + public List TopEvents { get; set; } = new(); } public class AnalyticsService : IAnalyticsService { - private readonly LogCentralDbContext _context; + private readonly LoggerDbContext _context; - public AnalyticsService(LogCentralDbContext context) + public AnalyticsService(LoggerDbContext context) { _context = context; } - public async Task> GetAnalyticsAsync(DateTime startDate, DateTime endDate) + public async Task GetAnalyticsAsync(DateTime startDate, DateTime endDate) { - var logs = await _context.LogEntries - .Where(l => l.Timestamp >= startDate && l.Timestamp <= endDate) + var analyticsLogs = await _context.LogEntries + .Where(l => l.Level == 7 && l.Timestamp >= startDate && l.Timestamp <= endDate) .ToListAsync(); - return new Dictionary + var topEvents = analyticsLogs + .GroupBy(l => l.Message) + .Select(g => new EventSummary + { + Event = g.Key, + Count = g.Count(), + LastSeen = g.Max(l => l.Timestamp).ToString("yyyy-MM-dd HH:mm:ss") + }) + .OrderByDescending(e => e.Count) + .Take(10) + .ToList(); + + var eventsByApp = analyticsLogs + .GroupBy(l => l.ApplicationName) + .ToDictionary(g => g.Key, g => g.Count()); + + var eventsByHour = analyticsLogs + .GroupBy(l => l.Timestamp.Hour) + .ToDictionary(g => $"{g.Key:D2}:00", g => g.Count()); + + var topUsers = analyticsLogs + .Where(l => !string.IsNullOrEmpty(l.UserId)) + .GroupBy(l => l.UserId!) + .Select(g => new UserActivity + { + UserId = g.Key, + EventCount = g.Count(), + TopEvents = g.GroupBy(l => l.Message) + .OrderByDescending(eg => eg.Count()) + .Take(3) + .Select(eg => eg.Key) + .ToList() + }) + .OrderByDescending(u => u.EventCount) + .Take(10) + .ToList(); + + return new AnalyticsResult { - ["totalEvents"] = logs.Count(l => l.Level == 7), - ["topEvents"] = logs - .Where(l => l.Level == 7) - .GroupBy(l => l.Message) - .Select(g => new { Event = g.Key, Count = g.Count() }) - .OrderByDescending(x => x.Count) - .Take(10) - .ToList() + TotalEvents = analyticsLogs.Count, + TopEvents = topEvents, + EventsByApplication = eventsByApp, + EventsByHour = eventsByHour, + TopUsers = topUsers }; } } \ No newline at end of file diff --git a/EonaCat.Logger.LogServer/Services/ILogService.cs b/EonaCat.Logger.LogServer/Services/ILogService.cs index 6701706..8920b34 100644 --- a/EonaCat.Logger.LogServer/Services/ILogService.cs +++ b/EonaCat.Logger.LogServer/Services/ILogService.cs @@ -1,6 +1,6 @@ -using LogCentral.Server.Models; +using EonaCat.Logger.Server.Models; -namespace LogCentral.Server.Services; +namespace EonaCat.Logger.Server.Services; public interface ILogService { @@ -9,6 +9,9 @@ public interface ILogService Task> GetLogsAsync(LogQueryParams queryParams); Task> GetStatsAsync(string? app, string? env); Task ValidateApiKeyAsync(string apiKey); + Task> GetApplicationsAsync(); + Task> GetEnvironmentsAsync(); + Task> GetLogTrendAsync(int hours); } public class LogQueryParams @@ -17,10 +20,15 @@ public class LogQueryParams public string? Application { get; set; } public string? Environment { get; set; } public int? Level { get; set; } + public string? Category { get; set; } public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } + public string? UserId { get; set; } + public string? CorrelationId { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 50; + public string? SortBy { get; set; } = "Timestamp"; + public string? SortOrder { get; set; } = "desc"; } public class PagedResult @@ -30,4 +38,11 @@ public class PagedResult public int Page { get; set; } public int PageSize { get; set; } public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); +} + +public class TrendPoint +{ + public DateTime Timestamp { get; set; } + public int Count { get; set; } + public Dictionary ByLevel { get; set; } = new(); } \ No newline at end of file diff --git a/EonaCat.Logger.LogServer/Services/ISearchService.cs b/EonaCat.Logger.LogServer/Services/ISearchService.cs index 6e788fc..9e61d7d 100644 --- a/EonaCat.Logger.LogServer/Services/ISearchService.cs +++ b/EonaCat.Logger.LogServer/Services/ISearchService.cs @@ -1,30 +1,89 @@ -using LogCentral.Server.Data; -using LogCentral.Server.Models; +using EonaCat.Logger.Server.Data; +using EonaCat.Logger.Server.Models; using Microsoft.EntityFrameworkCore; -namespace LogCentral.Server.Services; +namespace EonaCat.Logger.Server.Services; public interface ISearchService { - Task> SearchAsync(string query, int limit = 100); + Task SearchAsync(SearchRequest request); +} + +public class SearchRequest +{ + public string Query { get; set; } = string.Empty; + public List? Applications { get; set; } + public List? Environments { get; set; } + public List? Levels { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public int Limit { get; set; } = 100; +} + +public class SearchResult +{ + public List Results { get; set; } = new(); + public int TotalCount { get; set; } + public Dictionary Facets { get; set; } = new(); } public class SearchService : ISearchService { - private readonly LogCentralDbContext _context; + private readonly LoggerDbContext _context; - public SearchService(LogCentralDbContext context) + public SearchService(LoggerDbContext context) { _context = context; } - public async Task> SearchAsync(string query, int limit = 100) + public async Task SearchAsync(SearchRequest request) { - return await _context.LogEntries - .Where(l => EF.Functions.Like(l.Message, $"%{query}%") || - EF.Functions.Like(l.Exception ?? "", $"%{query}%")) + var query = _context.LogEntries.AsQueryable(); + + if (!string.IsNullOrEmpty(request.Query)) + { + var search = request.Query.ToLower(); + query = query.Where(l => + l.Message.ToLower().Contains(search) || + (l.Exception != null && l.Exception.ToLower().Contains(search)) || + (l.StackTrace != null && l.StackTrace.ToLower().Contains(search)) || + l.Category.ToLower().Contains(search) || + l.ApplicationName.ToLower().Contains(search) || + (l.UserId != null && l.UserId.ToLower().Contains(search)) || + (l.CorrelationId != null && l.CorrelationId.ToLower().Contains(search))); + } + + if (request.Applications?.Any() == true) + query = query.Where(l => request.Applications.Contains(l.ApplicationName)); + + if (request.Environments?.Any() == true) + query = query.Where(l => request.Environments.Contains(l.Environment)); + + if (request.Levels?.Any() == true) + query = query.Where(l => request.Levels.Contains(l.Level)); + + if (request.StartDate.HasValue) + query = query.Where(l => l.Timestamp >= request.StartDate.Value); + + if (request.EndDate.HasValue) + query = query.Where(l => l.Timestamp <= request.EndDate.Value); + + var totalCount = await query.CountAsync(); + var results = await query .OrderByDescending(l => l.Timestamp) - .Take(limit) + .Take(request.Limit) .ToListAsync(); + + var facets = await query + .GroupBy(l => l.Level) + .Select(g => new { Level = g.Key, Count = g.Count() }) + .ToDictionaryAsync(x => $"Level_{x.Level}", x => x.Count); + + return new SearchResult + { + Results = results, + TotalCount = totalCount, + Facets = facets + }; } } \ No newline at end of file diff --git a/EonaCat.Logger.LogServer/Services/LogService.cs b/EonaCat.Logger.LogServer/Services/LogService.cs index e6f9bf4..92b5959 100644 --- a/EonaCat.Logger.LogServer/Services/LogService.cs +++ b/EonaCat.Logger.LogServer/Services/LogService.cs @@ -1,23 +1,27 @@ -using Microsoft.EntityFrameworkCore; -using LogCentral.Server.Data; -using LogCentral.Server.Models; +using EonaCat.Logger.Server.Data; +using EonaCat.Logger.Server.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using System.Text.Json; -namespace LogCentral.Server.Services; +namespace EonaCat.Logger.Server.Services; public class LogService : ILogService { - private readonly LogCentralDbContext _context; + private readonly LoggerDbContext _context; + private readonly IMemoryCache _cache; - public LogService(LogCentralDbContext context) + public LogService(LoggerDbContext context, IMemoryCache cache) { _context = context; + _cache = cache; } public async Task AddLogsAsync(List entries) { await _context.LogEntries.AddRangeAsync(entries); await _context.SaveChangesAsync(); + _cache.Remove("stats"); } public async Task GetLogByIdAsync(string id) @@ -31,10 +35,12 @@ public class LogService : ILogService if (!string.IsNullOrEmpty(queryParams.Search)) { + var search = queryParams.Search.ToLower(); query = query.Where(l => - l.Message.Contains(queryParams.Search) || - l.Exception!.Contains(queryParams.Search) || - l.Category.Contains(queryParams.Search)); + l.Message.ToLower().Contains(search) || + (l.Exception != null && l.Exception.ToLower().Contains(search)) || + l.Category.ToLower().Contains(search) || + l.ApplicationName.ToLower().Contains(search)); } if (!string.IsNullOrEmpty(queryParams.Application)) @@ -43,6 +49,9 @@ public class LogService : ILogService if (!string.IsNullOrEmpty(queryParams.Environment)) query = query.Where(l => l.Environment == queryParams.Environment); + if (!string.IsNullOrEmpty(queryParams.Category)) + query = query.Where(l => l.Category == queryParams.Category); + if (queryParams.Level.HasValue) query = query.Where(l => l.Level == queryParams.Level.Value); @@ -52,9 +61,19 @@ public class LogService : ILogService if (queryParams.EndDate.HasValue) query = query.Where(l => l.Timestamp <= queryParams.EndDate.Value); + if (!string.IsNullOrEmpty(queryParams.UserId)) + query = query.Where(l => l.UserId == queryParams.UserId); + + if (!string.IsNullOrEmpty(queryParams.CorrelationId)) + query = query.Where(l => l.CorrelationId == queryParams.CorrelationId); + var totalCount = await query.CountAsync(); + + query = queryParams.SortOrder?.ToLower() == "asc" + ? query.OrderBy(l => l.Timestamp) + : query.OrderByDescending(l => l.Timestamp); + var items = await query - .OrderByDescending(l => l.Timestamp) .Skip((queryParams.Page - 1) * queryParams.PageSize) .Take(queryParams.PageSize) .ToListAsync(); @@ -70,6 +89,10 @@ public class LogService : ILogService public async Task> GetStatsAsync(string? app, string? env) { + var cacheKey = $"stats_{app}_{env}"; + if (_cache.TryGetValue(cacheKey, out Dictionary? cachedStats)) + return cachedStats!; + var query = _context.LogEntries.AsQueryable(); if (!string.IsNullOrEmpty(app)) @@ -79,28 +102,92 @@ public class LogService : ILogService query = query.Where(l => l.Environment == env); var last24Hours = DateTime.UtcNow.AddHours(-24); + var lastHour = DateTime.UtcNow.AddHours(-1); + var stats = new Dictionary { ["totalLogs"] = await query.CountAsync(), ["last24Hours"] = await query.Where(l => l.Timestamp >= last24Hours).CountAsync(), + ["lastHour"] = await query.Where(l => l.Timestamp >= lastHour).CountAsync(), ["errorCount"] = await query.Where(l => l.Level >= 4).CountAsync(), ["warningCount"] = await query.Where(l => l.Level == 3).CountAsync(), - ["applications"] = await _context.LogEntries - .Select(l => l.ApplicationName) - .Distinct() - .CountAsync(), + ["criticalCount"] = await query.Where(l => l.Level == 5).CountAsync(), + ["securityCount"] = await query.Where(l => l.Level == 6).CountAsync(), + ["applications"] = await _context.LogEntries.Select(l => l.ApplicationName).Distinct().CountAsync(), ["byLevel"] = await query .GroupBy(l => l.Level) .Select(g => new { Level = g.Key, Count = g.Count() }) + .ToListAsync(), + ["topErrors"] = await query + .Where(l => l.Level >= 4 && l.Timestamp >= last24Hours) + .GroupBy(l => l.Message) + .Select(g => new { Message = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(5) + .ToListAsync(), + ["topApplications"] = await query + .Where(l => l.Timestamp >= last24Hours) + .GroupBy(l => l.ApplicationName) + .Select(g => new { Application = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(5) .ToListAsync() }; + _cache.Set(cacheKey, stats, TimeSpan.FromSeconds(30)); return stats; } public async Task ValidateApiKeyAsync(string apiKey) { - return await _context.Applications - .AnyAsync(a => a.ApiKey == apiKey && a.IsActive); + return await _context.Applications.AnyAsync(a => a.ApiKey == apiKey && a.IsActive); + } + + public async Task> GetApplicationsAsync() + { + return await _context.LogEntries + .Select(l => l.ApplicationName) + .Distinct() + .OrderBy(a => a) + .ToListAsync(); + } + + public async Task> GetEnvironmentsAsync() + { + return await _context.LogEntries + .Select(l => l.Environment) + .Distinct() + .OrderBy(e => e) + .ToListAsync(); + } + + public async Task> GetLogTrendAsync(int hours) + { + var startTime = DateTime.UtcNow.AddHours(-hours); + var interval = hours > 24 ? 60 : hours > 6 ? 30 : 15; + + var logs = await _context.LogEntries + .Where(l => l.Timestamp >= startTime) + .ToListAsync(); + + var trend = logs + .GroupBy(l => new DateTime( + l.Timestamp.Year, + l.Timestamp.Month, + l.Timestamp.Day, + l.Timestamp.Hour, + (l.Timestamp.Minute / interval) * interval, + 0)) + .Select(g => new TrendPoint + { + Timestamp = g.Key, + Count = g.Count(), + ByLevel = g.GroupBy(l => l.Level) + .ToDictionary(lg => lg.Key, lg => lg.Count()) + }) + .OrderBy(t => t.Timestamp) + .ToList(); + + return trend; } } \ No newline at end of file