Updated
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
namespace LogCentral.Server.Controllers;
|
namespace EonaCat.Logger.Server.Controllers;
|
||||||
|
|
||||||
public class LogEntryDto
|
public class LogEntryDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using LogCentral.Server.Services;
|
using EonaCat.Logger.Server.Services;
|
||||||
using LogCentral.Server.Models;
|
using EonaCat.Logger.Server.Models;
|
||||||
|
|
||||||
namespace LogCentral.Server.Controllers;
|
namespace EonaCat.Logger.Server.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class LogsController : ControllerBase
|
public class LogsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ILogService _logService;
|
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;
|
_logService = logService;
|
||||||
|
_searchService = searchService;
|
||||||
|
_analyticsService = analyticsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("batch")]
|
[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();
|
var apiKey = Request.Headers["X-API-Key"].FirstOrDefault();
|
||||||
if (string.IsNullOrEmpty(apiKey) || !await _logService.ValidateApiKeyAsync(apiKey))
|
if (string.IsNullOrEmpty(apiKey) || !await _logService.ValidateApiKeyAsync(apiKey))
|
||||||
@@ -24,29 +28,7 @@ public class LogsController : ControllerBase
|
|||||||
return Unauthorized(new { error = "Invalid API key" });
|
return Unauthorized(new { error = "Invalid API key" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map DTO -> EF entity
|
await _logService.AddLogsAsync(entries);
|
||||||
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);
|
|
||||||
|
|
||||||
return Ok(new { success = true, count = entries.Count });
|
return Ok(new { success = true, count = entries.Count });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,4 +53,41 @@ public class LogsController : ControllerBase
|
|||||||
var stats = await _logService.GetStatsAsync(app, env);
|
var stats = await _logService.GetStatsAsync(app, env);
|
||||||
return Ok(stats);
|
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 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) { }
|
: base(options) { }
|
||||||
|
|
||||||
public DbSet<LogEntry> LogEntries { get; set; }
|
public DbSet<LogEntry> LogEntries { get; set; }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using LogCentral.Server.Data;
|
using EonaCat.Logger.Server.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.LogServer.Migrations
|
namespace EonaCat.Logger.LogServer.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(LogCentralDbContext))]
|
[DbContext(typeof(LoggerDbContext))]
|
||||||
[Migration("20260108140035_InitialCreate")]
|
[Migration("20260108140035_InitialCreate")]
|
||||||
partial class InitialCreate
|
partial class InitialCreate
|
||||||
{
|
{
|
||||||
@@ -20,7 +20,7 @@ namespace EonaCat.Logger.LogServer.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||||
|
|
||||||
modelBuilder.Entity("LogCentral.Server.Models.Application", b =>
|
modelBuilder.Entity("Logger.Server.Models.Application", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -48,7 +48,7 @@ namespace EonaCat.Logger.LogServer.Migrations
|
|||||||
b.ToTable("Applications");
|
b.ToTable("Applications");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("LogCentral.Server.Models.LogEntry", b =>
|
modelBuilder.Entity("Logger.Server.Models.LogEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using LogCentral.Server.Data;
|
using EonaCat.Logger.Server.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
@@ -9,15 +9,15 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
|
|
||||||
namespace EonaCat.Logger.LogServer.Migrations
|
namespace EonaCat.Logger.LogServer.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(LogCentralDbContext))]
|
[DbContext(typeof(LoggerDbContext))]
|
||||||
partial class LogCentralDbContextModelSnapshot : ModelSnapshot
|
partial class LoggerDbContextModelSnapshot : ModelSnapshot
|
||||||
{
|
{
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||||
|
|
||||||
modelBuilder.Entity("LogCentral.Server.Models.Application", b =>
|
modelBuilder.Entity("Logger.Server.Models.Application", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -45,7 +45,7 @@ namespace EonaCat.Logger.LogServer.Migrations
|
|||||||
b.ToTable("Applications");
|
b.ToTable("Applications");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("LogCentral.Server.Models.LogEntry", b =>
|
modelBuilder.Entity("Logger.Server.Models.LogEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.Rendering;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace LogCentral.Server.Models;
|
namespace EonaCat.Logger.Server.Models;
|
||||||
|
|
||||||
public class LogEntry
|
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>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<script type="importmap"></script>
|
||||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
<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="~/css/site.css" asp-append-version="true" />
|
||||||
<link rel="stylesheet" href="~/EonaCat.Logger.LogServer.styles.css" asp-append-version="true" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<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>
|
</header>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<main role="main" class="pb-3">
|
<main role="main" class="pb-3">
|
||||||
@@ -39,7 +20,7 @@
|
|||||||
|
|
||||||
<footer class="border-top footer text-muted">
|
<footer class="border-top footer text-muted">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
© 2026 - EonaCat.Logger.LogServer - <a asp-area="" asp-page="/Privacy">Privacy</a>
|
© @DateTime.Now.Year - EonaCat (Jeroen Saey)
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
using LogCentral.Server.Data;
|
using EonaCat.Logger.Server.Data;
|
||||||
using LogCentral.Server.Services;
|
using EonaCat.Logger.Server.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddDbContext<LogCentralDbContext>(options =>
|
builder.Services.AddDbContext<LoggerDbContext>(options =>
|
||||||
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
|
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
builder.Services.AddScoped<ILogService, LogService>();
|
builder.Services.AddScoped<ILogService, LogService>();
|
||||||
builder.Services.AddScoped<ISearchService, SearchService>();
|
builder.Services.AddScoped<ISearchService, SearchService>();
|
||||||
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
|
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var db = scope.ServiceProvider.GetRequiredService<LogCentralDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<LoggerDbContext>();
|
||||||
db.Database.EnsureCreated();
|
db.Database.EnsureCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,94 @@
|
|||||||
using LogCentral.Server.Data;
|
using EonaCat.Logger.Server.Data;
|
||||||
using LogCentral.Server.Services;
|
using EonaCat.Logger.Server.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
public interface IAnalyticsService
|
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
|
public class AnalyticsService : IAnalyticsService
|
||||||
{
|
{
|
||||||
private readonly LogCentralDbContext _context;
|
private readonly LoggerDbContext _context;
|
||||||
|
|
||||||
public AnalyticsService(LogCentralDbContext context)
|
public AnalyticsService(LoggerDbContext context)
|
||||||
{
|
{
|
||||||
_context = 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
|
var analyticsLogs = await _context.LogEntries
|
||||||
.Where(l => l.Timestamp >= startDate && l.Timestamp <= endDate)
|
.Where(l => l.Level == 7 && l.Timestamp >= startDate && l.Timestamp <= endDate)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return new Dictionary<string, object>
|
var topEvents = analyticsLogs
|
||||||
{
|
|
||||||
["totalEvents"] = logs.Count(l => l.Level == 7),
|
|
||||||
["topEvents"] = logs
|
|
||||||
.Where(l => l.Level == 7)
|
|
||||||
.GroupBy(l => l.Message)
|
.GroupBy(l => l.Message)
|
||||||
.Select(g => new { Event = g.Key, Count = g.Count() })
|
.Select(g => new EventSummary
|
||||||
.OrderByDescending(x => x.Count)
|
{
|
||||||
|
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)
|
.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()
|
.ToList()
|
||||||
|
})
|
||||||
|
.OrderByDescending(u => u.EventCount)
|
||||||
|
.Take(10)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new AnalyticsResult
|
||||||
|
{
|
||||||
|
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
|
public interface ILogService
|
||||||
{
|
{
|
||||||
@@ -9,6 +9,9 @@ public interface ILogService
|
|||||||
Task<PagedResult<LogEntry>> GetLogsAsync(LogQueryParams queryParams);
|
Task<PagedResult<LogEntry>> GetLogsAsync(LogQueryParams queryParams);
|
||||||
Task<Dictionary<string, object>> GetStatsAsync(string? app, string? env);
|
Task<Dictionary<string, object>> GetStatsAsync(string? app, string? env);
|
||||||
Task<bool> ValidateApiKeyAsync(string apiKey);
|
Task<bool> ValidateApiKeyAsync(string apiKey);
|
||||||
|
Task<List<string>> GetApplicationsAsync();
|
||||||
|
Task<List<string>> GetEnvironmentsAsync();
|
||||||
|
Task<List<TrendPoint>> GetLogTrendAsync(int hours);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LogQueryParams
|
public class LogQueryParams
|
||||||
@@ -17,10 +20,15 @@ public class LogQueryParams
|
|||||||
public string? Application { get; set; }
|
public string? Application { get; set; }
|
||||||
public string? Environment { get; set; }
|
public string? Environment { get; set; }
|
||||||
public int? Level { get; set; }
|
public int? Level { get; set; }
|
||||||
|
public string? Category { get; set; }
|
||||||
public DateTime? StartDate { get; set; }
|
public DateTime? StartDate { get; set; }
|
||||||
public DateTime? EndDate { 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 Page { get; set; } = 1;
|
||||||
public int PageSize { get; set; } = 50;
|
public int PageSize { get; set; } = 50;
|
||||||
|
public string? SortBy { get; set; } = "Timestamp";
|
||||||
|
public string? SortOrder { get; set; } = "desc";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PagedResult<T>
|
public class PagedResult<T>
|
||||||
@@ -31,3 +39,10 @@ public class PagedResult<T>
|
|||||||
public int PageSize { get; set; }
|
public int PageSize { get; set; }
|
||||||
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
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 EonaCat.Logger.Server.Data;
|
||||||
using LogCentral.Server.Models;
|
using EonaCat.Logger.Server.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace LogCentral.Server.Services;
|
namespace EonaCat.Logger.Server.Services;
|
||||||
|
|
||||||
public interface ISearchService
|
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
|
public class SearchService : ISearchService
|
||||||
{
|
{
|
||||||
private readonly LogCentralDbContext _context;
|
private readonly LoggerDbContext _context;
|
||||||
|
|
||||||
public SearchService(LogCentralDbContext context)
|
public SearchService(LoggerDbContext context)
|
||||||
{
|
{
|
||||||
_context = 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
|
var query = _context.LogEntries.AsQueryable();
|
||||||
.Where(l => EF.Functions.Like(l.Message, $"%{query}%") ||
|
|
||||||
EF.Functions.Like(l.Exception ?? "", $"%{query}%"))
|
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)
|
.OrderByDescending(l => l.Timestamp)
|
||||||
.Take(limit)
|
.Take(request.Limit)
|
||||||
.ToListAsync();
|
.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 EonaCat.Logger.Server.Data;
|
||||||
using LogCentral.Server.Data;
|
using EonaCat.Logger.Server.Models;
|
||||||
using LogCentral.Server.Models;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace LogCentral.Server.Services;
|
namespace EonaCat.Logger.Server.Services;
|
||||||
|
|
||||||
public class LogService : ILogService
|
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;
|
_context = context;
|
||||||
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddLogsAsync(List<LogEntry> entries)
|
public async Task AddLogsAsync(List<LogEntry> entries)
|
||||||
{
|
{
|
||||||
await _context.LogEntries.AddRangeAsync(entries);
|
await _context.LogEntries.AddRangeAsync(entries);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
_cache.Remove("stats");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogEntry?> GetLogByIdAsync(string id)
|
public async Task<LogEntry?> GetLogByIdAsync(string id)
|
||||||
@@ -31,10 +35,12 @@ public class LogService : ILogService
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(queryParams.Search))
|
if (!string.IsNullOrEmpty(queryParams.Search))
|
||||||
{
|
{
|
||||||
|
var search = queryParams.Search.ToLower();
|
||||||
query = query.Where(l =>
|
query = query.Where(l =>
|
||||||
l.Message.Contains(queryParams.Search) ||
|
l.Message.ToLower().Contains(search) ||
|
||||||
l.Exception!.Contains(queryParams.Search) ||
|
(l.Exception != null && l.Exception.ToLower().Contains(search)) ||
|
||||||
l.Category.Contains(queryParams.Search));
|
l.Category.ToLower().Contains(search) ||
|
||||||
|
l.ApplicationName.ToLower().Contains(search));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(queryParams.Application))
|
if (!string.IsNullOrEmpty(queryParams.Application))
|
||||||
@@ -43,6 +49,9 @@ public class LogService : ILogService
|
|||||||
if (!string.IsNullOrEmpty(queryParams.Environment))
|
if (!string.IsNullOrEmpty(queryParams.Environment))
|
||||||
query = query.Where(l => l.Environment == 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)
|
if (queryParams.Level.HasValue)
|
||||||
query = query.Where(l => l.Level == queryParams.Level.Value);
|
query = query.Where(l => l.Level == queryParams.Level.Value);
|
||||||
|
|
||||||
@@ -52,9 +61,19 @@ public class LogService : ILogService
|
|||||||
if (queryParams.EndDate.HasValue)
|
if (queryParams.EndDate.HasValue)
|
||||||
query = query.Where(l => l.Timestamp <= queryParams.EndDate.Value);
|
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();
|
var totalCount = await query.CountAsync();
|
||||||
|
|
||||||
|
query = queryParams.SortOrder?.ToLower() == "asc"
|
||||||
|
? query.OrderBy(l => l.Timestamp)
|
||||||
|
: query.OrderByDescending(l => l.Timestamp);
|
||||||
|
|
||||||
var items = await query
|
var items = await query
|
||||||
.OrderByDescending(l => l.Timestamp)
|
|
||||||
.Skip((queryParams.Page - 1) * queryParams.PageSize)
|
.Skip((queryParams.Page - 1) * queryParams.PageSize)
|
||||||
.Take(queryParams.PageSize)
|
.Take(queryParams.PageSize)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -70,6 +89,10 @@ public class LogService : ILogService
|
|||||||
|
|
||||||
public async Task<Dictionary<string, object>> GetStatsAsync(string? app, string? env)
|
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();
|
var query = _context.LogEntries.AsQueryable();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(app))
|
if (!string.IsNullOrEmpty(app))
|
||||||
@@ -79,28 +102,92 @@ public class LogService : ILogService
|
|||||||
query = query.Where(l => l.Environment == env);
|
query = query.Where(l => l.Environment == env);
|
||||||
|
|
||||||
var last24Hours = DateTime.UtcNow.AddHours(-24);
|
var last24Hours = DateTime.UtcNow.AddHours(-24);
|
||||||
|
var lastHour = DateTime.UtcNow.AddHours(-1);
|
||||||
|
|
||||||
var stats = new Dictionary<string, object>
|
var stats = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["totalLogs"] = await query.CountAsync(),
|
["totalLogs"] = await query.CountAsync(),
|
||||||
["last24Hours"] = await query.Where(l => l.Timestamp >= last24Hours).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(),
|
["errorCount"] = await query.Where(l => l.Level >= 4).CountAsync(),
|
||||||
["warningCount"] = await query.Where(l => l.Level == 3).CountAsync(),
|
["warningCount"] = await query.Where(l => l.Level == 3).CountAsync(),
|
||||||
["applications"] = await _context.LogEntries
|
["criticalCount"] = await query.Where(l => l.Level == 5).CountAsync(),
|
||||||
.Select(l => l.ApplicationName)
|
["securityCount"] = await query.Where(l => l.Level == 6).CountAsync(),
|
||||||
.Distinct()
|
["applications"] = await _context.LogEntries.Select(l => l.ApplicationName).Distinct().CountAsync(),
|
||||||
.CountAsync(),
|
|
||||||
["byLevel"] = await query
|
["byLevel"] = await query
|
||||||
.GroupBy(l => l.Level)
|
.GroupBy(l => l.Level)
|
||||||
.Select(g => new { Level = g.Key, Count = g.Count() })
|
.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()
|
.ToListAsync()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_cache.Set(cacheKey, stats, TimeSpan.FromSeconds(30));
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ValidateApiKeyAsync(string apiKey)
|
public async Task<bool> ValidateApiKeyAsync(string apiKey)
|
||||||
{
|
{
|
||||||
return await _context.Applications
|
return await _context.Applications.AnyAsync(a => a.ApiKey == apiKey && a.IsActive);
|
||||||
.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