Updated
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
namespace LogCentral.Server.Controllers;
|
||||
namespace EonaCat.Logger.Server.Controllers;
|
||||
|
||||
public class LogEntryDto
|
||||
{
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using LogCentral.Server.Services;
|
||||
using LogCentral.Server.Models;
|
||||
using EonaCat.Logger.Server.Services;
|
||||
using EonaCat.Logger.Server.Models;
|
||||
|
||||
namespace LogCentral.Server.Controllers;
|
||||
namespace EonaCat.Logger.Server.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class LogsController : ControllerBase
|
||||
{
|
||||
private readonly ILogService _logService;
|
||||
private readonly ISearchService _searchService;
|
||||
private readonly IAnalyticsService _analyticsService;
|
||||
|
||||
public LogsController(ILogService logService)
|
||||
public LogsController(ILogService logService, ISearchService searchService, IAnalyticsService analyticsService)
|
||||
{
|
||||
_logService = logService;
|
||||
_searchService = searchService;
|
||||
_analyticsService = analyticsService;
|
||||
}
|
||||
|
||||
[HttpPost("batch")]
|
||||
public async Task<IActionResult> PostBatch([FromBody] List<LogEntryDto> entries)
|
||||
public async Task<IActionResult> PostBatch([FromBody] List<LogEntry> entries)
|
||||
{
|
||||
var apiKey = Request.Headers["X-API-Key"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(apiKey) || !await _logService.ValidateApiKeyAsync(apiKey))
|
||||
@@ -24,29 +28,7 @@ public class LogsController : ControllerBase
|
||||
return Unauthorized(new { error = "Invalid API key" });
|
||||
}
|
||||
|
||||
// Map DTO -> EF entity
|
||||
var logEntities = entries.Select(dto => new LogEntry
|
||||
{
|
||||
Id = dto.Id,
|
||||
Timestamp = dto.Timestamp,
|
||||
ApplicationName = dto.ApplicationName,
|
||||
ApplicationVersion = dto.ApplicationVersion,
|
||||
Environment = dto.Environment,
|
||||
MachineName = dto.MachineName,
|
||||
Level = dto.Level,
|
||||
Category = dto.Category,
|
||||
Message = dto.Message,
|
||||
Exception = dto.Exception,
|
||||
StackTrace = dto.StackTrace,
|
||||
Properties = dto.Properties,
|
||||
UserId = dto.UserId,
|
||||
SessionId = dto.SessionId,
|
||||
RequestId = dto.RequestId,
|
||||
CorrelationId = dto.CorrelationId
|
||||
}).ToList();
|
||||
|
||||
await _logService.AddLogsAsync(logEntities);
|
||||
|
||||
await _logService.AddLogsAsync(entries);
|
||||
return Ok(new { success = true, count = entries.Count });
|
||||
}
|
||||
|
||||
@@ -71,4 +53,41 @@ public class LogsController : ControllerBase
|
||||
var stats = await _logService.GetStatsAsync(app, env);
|
||||
return Ok(stats);
|
||||
}
|
||||
|
||||
[HttpGet("applications")]
|
||||
public async Task<IActionResult> GetApplications()
|
||||
{
|
||||
var apps = await _logService.GetApplicationsAsync();
|
||||
return Ok(apps);
|
||||
}
|
||||
|
||||
[HttpGet("environments")]
|
||||
public async Task<IActionResult> GetEnvironments()
|
||||
{
|
||||
var envs = await _logService.GetEnvironmentsAsync();
|
||||
return Ok(envs);
|
||||
}
|
||||
|
||||
[HttpGet("trend")]
|
||||
public async Task<IActionResult> GetTrend([FromQuery] int hours = 24)
|
||||
{
|
||||
var trend = await _logService.GetLogTrendAsync(hours);
|
||||
return Ok(trend);
|
||||
}
|
||||
|
||||
[HttpPost("search")]
|
||||
public async Task<IActionResult> Search([FromBody] SearchRequest request)
|
||||
{
|
||||
var results = await _searchService.SearchAsync(request);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpGet("analytics")]
|
||||
public async Task<IActionResult> GetAnalytics([FromQuery] DateTime? start, [FromQuery] DateTime? end)
|
||||
{
|
||||
var startDate = start ?? DateTime.UtcNow.AddDays(-7);
|
||||
var endDate = end ?? DateTime.UtcNow;
|
||||
var analytics = await _analyticsService.GetAnalyticsAsync(startDate, endDate);
|
||||
return Ok(analytics);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LogCentral.Server.Models;
|
||||
using EonaCat.Logger.Server.Models;
|
||||
|
||||
namespace LogCentral.Server.Data;
|
||||
namespace EonaCat.Logger.Server.Data;
|
||||
|
||||
public class LogCentralDbContext : DbContext
|
||||
public class LoggerDbContext : DbContext
|
||||
{
|
||||
public LogCentralDbContext(DbContextOptions<LogCentralDbContext> options)
|
||||
public LoggerDbContext(DbContextOptions<LoggerDbContext> options)
|
||||
: base(options) { }
|
||||
|
||||
public DbSet<LogEntry> LogEntries { get; set; }
|
||||
@@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LogCentral.Server.Data;
|
||||
using EonaCat.Logger.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace EonaCat.Logger.LogServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LogCentralDbContext))]
|
||||
[DbContext(typeof(LoggerDbContext))]
|
||||
[Migration("20260108140035_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
@@ -20,7 +20,7 @@ namespace EonaCat.Logger.LogServer.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||
|
||||
modelBuilder.Entity("LogCentral.Server.Models.Application", b =>
|
||||
modelBuilder.Entity("Logger.Server.Models.Application", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -48,7 +48,7 @@ namespace EonaCat.Logger.LogServer.Migrations
|
||||
b.ToTable("Applications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LogCentral.Server.Models.LogEntry", b =>
|
||||
modelBuilder.Entity("Logger.Server.Models.LogEntry", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LogCentral.Server.Data;
|
||||
using EonaCat.Logger.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -9,15 +9,15 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace EonaCat.Logger.LogServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LogCentralDbContext))]
|
||||
partial class LogCentralDbContextModelSnapshot : ModelSnapshot
|
||||
[DbContext(typeof(LoggerDbContext))]
|
||||
partial class LoggerDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||
|
||||
modelBuilder.Entity("LogCentral.Server.Models.Application", b =>
|
||||
modelBuilder.Entity("Logger.Server.Models.Application", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -45,7 +45,7 @@ namespace EonaCat.Logger.LogServer.Migrations
|
||||
b.ToTable("Applications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LogCentral.Server.Models.LogEntry", b =>
|
||||
modelBuilder.Entity("Logger.Server.Models.LogEntry", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LogCentral.Server.Models;
|
||||
namespace EonaCat.Logger.Server.Models;
|
||||
|
||||
public class LogEntry
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
@page
|
||||
@model PrivacyModel
|
||||
@{
|
||||
ViewData["Title"] = "Privacy Policy";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
|
||||
<p>Use this page to detail your site's privacy policy.</p>
|
||||
@@ -1,13 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace EonaCat.Logger.LogServer.Pages
|
||||
{
|
||||
public class PrivacyModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,33 +3,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - EonaCat.Logger.LogServer</title>
|
||||
<title>@ViewData["Title"]</title>
|
||||
<script type="importmap"></script>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/EonaCat.Logger.LogServer.styles.css" asp-append-version="true" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" asp-area="" asp-page="/Index">EonaCat.Logger.LogServer</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
<div class="container">
|
||||
<main role="main" class="pb-3">
|
||||
@@ -39,7 +20,7 @@
|
||||
|
||||
<footer class="border-top footer text-muted">
|
||||
<div class="container">
|
||||
© 2026 - EonaCat.Logger.LogServer - <a asp-area="" asp-page="/Privacy">Privacy</a>
|
||||
© @DateTime.Now.Year - EonaCat (Jeroen Saey)
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -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<LogCentralDbContext>(options =>
|
||||
builder.Services.AddDbContext<LoggerDbContext>(options =>
|
||||
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
builder.Services.AddScoped<ILogService, LogService>();
|
||||
builder.Services.AddScoped<ISearchService, SearchService>();
|
||||
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
|
||||
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<LogCentralDbContext>();
|
||||
var db = scope.ServiceProvider.GetRequiredService<LoggerDbContext>();
|
||||
db.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Dictionary<string, object>> GetAnalyticsAsync(DateTime startDate, DateTime endDate);
|
||||
Task<AnalyticsResult> GetAnalyticsAsync(DateTime startDate, DateTime endDate);
|
||||
}
|
||||
|
||||
public class AnalyticsResult
|
||||
{
|
||||
public int TotalEvents { get; set; }
|
||||
public List<EventSummary> TopEvents { get; set; } = new();
|
||||
public Dictionary<string, int> EventsByApplication { get; set; } = new();
|
||||
public Dictionary<string, int> EventsByHour { get; set; } = new();
|
||||
public List<UserActivity> 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<string> 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<Dictionary<string, object>> GetAnalyticsAsync(DateTime startDate, DateTime endDate)
|
||||
public async Task<AnalyticsResult> 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<string, object>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<PagedResult<LogEntry>> GetLogsAsync(LogQueryParams queryParams);
|
||||
Task<Dictionary<string, object>> GetStatsAsync(string? app, string? env);
|
||||
Task<bool> ValidateApiKeyAsync(string apiKey);
|
||||
Task<List<string>> GetApplicationsAsync();
|
||||
Task<List<string>> GetEnvironmentsAsync();
|
||||
Task<List<TrendPoint>> 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<T>
|
||||
@@ -30,4 +38,11 @@ public class PagedResult<T>
|
||||
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<int, int> ByLevel { get; set; } = new();
|
||||
}
|
||||
@@ -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<List<LogEntry>> SearchAsync(string query, int limit = 100);
|
||||
Task<SearchResult> SearchAsync(SearchRequest request);
|
||||
}
|
||||
|
||||
public class SearchRequest
|
||||
{
|
||||
public string Query { get; set; } = string.Empty;
|
||||
public List<string>? Applications { get; set; }
|
||||
public List<string>? Environments { get; set; }
|
||||
public List<int>? Levels { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public int Limit { get; set; } = 100;
|
||||
}
|
||||
|
||||
public class SearchResult
|
||||
{
|
||||
public List<LogEntry> Results { get; set; } = new();
|
||||
public int TotalCount { get; set; }
|
||||
public Dictionary<string, int> 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<List<LogEntry>> SearchAsync(string query, int limit = 100)
|
||||
public async Task<SearchResult> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<LogEntry> entries)
|
||||
{
|
||||
await _context.LogEntries.AddRangeAsync(entries);
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove("stats");
|
||||
}
|
||||
|
||||
public async Task<LogEntry?> 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<Dictionary<string, object>> GetStatsAsync(string? app, string? env)
|
||||
{
|
||||
var cacheKey = $"stats_{app}_{env}";
|
||||
if (_cache.TryGetValue(cacheKey, out Dictionary<string, object>? 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<string, object>
|
||||
{
|
||||
["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<bool> 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<List<string>> GetApplicationsAsync()
|
||||
{
|
||||
return await _context.LogEntries
|
||||
.Select(l => l.ApplicationName)
|
||||
.Distinct()
|
||||
.OrderBy(a => a)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetEnvironmentsAsync()
|
||||
{
|
||||
return await _context.LogEntries
|
||||
.Select(l => l.Environment)
|
||||
.Distinct()
|
||||
.OrderBy(e => e)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TrendPoint>> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user