This commit is contained in:
2026-01-08 19:03:40 +01:00
parent 2e0cac4c41
commit 448b8da5c3
15 changed files with 1254 additions and 317 deletions

View File

@@ -1,4 +1,4 @@
namespace LogCentral.Server.Controllers;
namespace EonaCat.Logger.Server.Controllers;
public class LogEntryDto
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">
&copy; 2026 - EonaCat.Logger.LogServer - <a asp-area="" asp-page="/Privacy">Privacy</a>
&copy; @DateTime.Now.Year - EonaCat (Jeroen Saey)
</div>
</footer>

View File

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

View File

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

View File

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

View File

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

View File

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