Added LogHub
This commit is contained in:
@@ -28,6 +28,7 @@ public class LogsController : ControllerBase
|
|||||||
return Unauthorized(new { error = "Invalid API key" });
|
return Unauthorized(new { error = "Invalid API key" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await _logService.AddLogsAsync(entries);
|
await _logService.AddLogsAsync(entries);
|
||||||
return Ok(new { success = true, count = entries.Count });
|
return Ok(new { success = true, count = entries.Count });
|
||||||
}
|
}
|
||||||
@@ -48,9 +49,9 @@ public class LogsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("stats")]
|
[HttpGet("stats")]
|
||||||
public async Task<IActionResult> GetStats([FromQuery] string? app, [FromQuery] string? env)
|
public async Task<IActionResult> GetStats([FromQuery] string? app, [FromQuery] string? env, [FromQuery] bool skipCache = false)
|
||||||
{
|
{
|
||||||
var stats = await _logService.GetStatsAsync(app, env);
|
var stats = await _logService.GetStatsAsync(app, env, skipCache);
|
||||||
return Ok(stats);
|
return Ok(stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
EonaCat.Logger.LogServer/Data/LogDispatcher.cs
Normal file
31
EonaCat.Logger.LogServer/Data/LogDispatcher.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using EonaCat.Logger.LogServer.Hubs;
|
||||||
|
using EonaCat.Logger.Server.Models;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace EonaCat.Logger.LogServer.Data
|
||||||
|
{
|
||||||
|
public class LogDispatcher
|
||||||
|
{
|
||||||
|
private readonly IHubContext<LogHub> _hub;
|
||||||
|
|
||||||
|
public LogDispatcher(IHubContext<LogHub> hub)
|
||||||
|
{
|
||||||
|
_hub = hub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PublishAsync(LogEntry log)
|
||||||
|
{
|
||||||
|
await _hub.Clients.All.SendAsync("NewLog", new
|
||||||
|
{
|
||||||
|
id = log.Id,
|
||||||
|
level = log.Level,
|
||||||
|
message = log.Message,
|
||||||
|
applicationName = log.ApplicationName,
|
||||||
|
environment = log.Environment,
|
||||||
|
timestamp = log.Timestamp,
|
||||||
|
machineName = log.MachineName,
|
||||||
|
correlationId = log.CorrelationId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EonaCat.Json" Version="1.2.0" />
|
<PackageReference Include="EonaCat.Json" Version="1.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
13
EonaCat.Logger.LogServer/Hubs/LogHub.cs
Normal file
13
EonaCat.Logger.LogServer/Hubs/LogHub.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace EonaCat.Logger.LogServer.Hubs
|
||||||
|
{
|
||||||
|
public class LogHub : Hub
|
||||||
|
{
|
||||||
|
public override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("Connected", Context.ConnectionId);
|
||||||
|
await base.OnConnectedAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -356,6 +356,13 @@
|
|||||||
|
|
||||||
<!-- Logs Tab -->
|
<!-- Logs Tab -->
|
||||||
<div id="logs-tab" class="tab-content hidden">
|
<div id="logs-tab" class="tab-content hidden">
|
||||||
|
<button id="live-toggle"
|
||||||
|
onclick="toggleLive()"
|
||||||
|
class="px-4 py-2 rounded-lg bg-green-600 text-white font-semibold flex items-center gap-2">
|
||||||
|
<span id="live-dot" class="pulse-dot">●</span>
|
||||||
|
Live
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Advanced Filters -->
|
<!-- Advanced Filters -->
|
||||||
<div class="bg-white rounded-xl shadow-lg p-6 mb-6">
|
<div class="bg-white rounded-xl shadow-lg p-6 mb-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
@@ -556,6 +563,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
let liveEnabled = false;
|
||||||
|
let signalRConnection = null;
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let totalPages = 1;
|
let totalPages = 1;
|
||||||
let levelChart, trendChart;
|
let levelChart, trendChart;
|
||||||
@@ -566,9 +575,35 @@
|
|||||||
const levelClasses = ['trace', 'debug', 'info', 'warning', 'error', 'critical', 'security', 'analytics'];
|
const levelClasses = ['trace', 'debug', 'info', 'warning', 'error', 'critical', 'security', 'analytics'];
|
||||||
const levelColors = ['#9ca3af', '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#a855f7', '#f97316', '#14b8a6'];
|
const levelColors = ['#9ca3af', '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#a855f7', '#f97316', '#14b8a6'];
|
||||||
|
|
||||||
async function loadStats() {
|
async function initSignalR() {
|
||||||
try {
|
signalRConnection = new signalR.HubConnectionBuilder()
|
||||||
const response = await fetch('/api/logs/stats');
|
.withUrl("/logHub")
|
||||||
|
.withAutomaticReconnect()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
signalRConnection.on("Connected", id => {
|
||||||
|
console.log("Connected with ID:", id);
|
||||||
|
});
|
||||||
|
|
||||||
|
signalRConnection.on("NewLogBatch", logs =>
|
||||||
|
{
|
||||||
|
if (!liveEnabled) return;
|
||||||
|
logs.forEach(log => prependLiveLog(log));
|
||||||
|
});
|
||||||
|
|
||||||
|
signalRConnection.onreconnected(() => {
|
||||||
|
console.log("SignalR reconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
await signalRConnection.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
let statsInterval;
|
||||||
|
async function loadStats(skipCache = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const response = await fetch('/api/logs/stats' + (skipCache ? '?skipCache=true' : ''));
|
||||||
const stats = await response.json();
|
const stats = await response.json();
|
||||||
|
|
||||||
document.getElementById('stat-total').textContent = stats.totalLogs.toLocaleString();
|
document.getElementById('stat-total').textContent = stats.totalLogs.toLocaleString();
|
||||||
@@ -651,28 +686,42 @@
|
|||||||
|
|
||||||
function displayTopApplications(apps) {
|
function displayTopApplications(apps) {
|
||||||
const container = document.getElementById('top-apps');
|
const container = document.getElementById('top-apps');
|
||||||
|
|
||||||
if (!apps || apps.length === 0) {
|
if (!apps || apps.length === 0) {
|
||||||
container.innerHTML = '<p class="text-gray-500 text-center py-8">No data available</p>';
|
container.innerHTML = '<p class="text-gray-500 text-center py-8">No data available</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxCount = Math.max(...apps.map(a => a.count));
|
const maxCount = Math.max(...apps.map(a => a.count), 1); // prevent division by zero
|
||||||
container.innerHTML = apps.map(app => `
|
|
||||||
|
container.innerHTML = apps.map(app => {
|
||||||
|
const widthPercent = Math.round((app.count / maxCount) * 100);
|
||||||
|
return `
|
||||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition">
|
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition">
|
||||||
<div class="flex items-center gap-3 flex-1">
|
<div class="flex items-center gap-3 flex-1">
|
||||||
<div class="bg-blue-100 rounded-lg p-2">
|
<div class="bg-blue-100 rounded-lg p-2">
|
||||||
<i class="fas fa-cube text-blue-600"></i>
|
<i class="fas fa-cube text-blue-600"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-semibold text-gray-800">${app.application}</p>
|
<p class="font-semibold text-gray-800">${escapeHtml(app.application)}</p>
|
||||||
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
|
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
|
||||||
<div class="bg-blue-600 h-2 rounded-full transition-all" style="width: ${(app.count / maxCount) * 100}%"></div>
|
<div class="bg-blue-600 h-2 rounded-full transition-all" style="width: ${widthPercent}%;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge bg-blue-100 text-blue-800">${app.count.toLocaleString()}</span>
|
<span class="badge bg-blue-100 text-blue-800">${app.count.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayRecentErrors(errors) {
|
function displayRecentErrors(errors) {
|
||||||
@@ -1099,8 +1148,86 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadStats();
|
let totalLogs = 0;
|
||||||
|
|
||||||
|
function prependLiveLog(log) {
|
||||||
|
const container = document.getElementById('logs-container');
|
||||||
|
const totalCounter = document.getElementById('log-count');
|
||||||
|
|
||||||
|
// Check if any filters are active
|
||||||
|
const filtersActive = document.getElementById('active-filters')?.children.length > 0;
|
||||||
|
|
||||||
|
// Increment total logs counter always
|
||||||
|
totalLogs++;
|
||||||
|
if (totalCounter) {
|
||||||
|
const currentCount = parseInt(totalCounter.textContent.replace(/,/g, '')) || 0;
|
||||||
|
totalCounter.textContent = (currentCount + 1).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If filters are active, skip prepending to DOM
|
||||||
|
if (filtersActive) return;
|
||||||
|
|
||||||
|
// Create log element
|
||||||
|
const level = log.level;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
|
||||||
|
el.className = `log-entry log-${levelClasses[level]} border-l-4 animate-pulse`;
|
||||||
|
el.onclick = () => showLogDetail(log.id);
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="flex justify-between items-start mb-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="badge bg-white shadow-sm">
|
||||||
|
${levelIcons[level]} ${levelNames[level]}
|
||||||
|
</span>
|
||||||
|
<span class="font-semibold">${escapeHtml(log.applicationName)}</span>
|
||||||
|
<span class="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
|
||||||
|
LIVE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
${new Date(log.timestamp).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="font-medium">${escapeHtml(log.message)}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Prepend to container
|
||||||
|
container.prepend(el);
|
||||||
|
|
||||||
|
// Remove the pulse animation after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
el.classList.remove('animate-pulse');
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Limit DOM size
|
||||||
|
if (container.children.length > 200) {
|
||||||
|
container.removeChild(container.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLive() {
|
||||||
|
liveEnabled = !liveEnabled;
|
||||||
|
|
||||||
|
const btn = document.getElementById('live-toggle');
|
||||||
|
const dot = document.getElementById('live-dot');
|
||||||
|
|
||||||
|
if (liveEnabled) {
|
||||||
|
btn.classList.remove('bg-green-600');
|
||||||
|
btn.classList.add('bg-red-600');
|
||||||
|
btn.innerHTML = `<span id="live-dot" class="pulse-dot">●</span> Live`;
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('bg-red-600');
|
||||||
|
btn.classList.add('bg-green-600');
|
||||||
|
btn.innerHTML = `<span>●</span> Live`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initSignalR();
|
||||||
|
toggleLive();
|
||||||
|
loadStats(true);
|
||||||
loadTrend();
|
loadTrend();
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!document.getElementById('dashboard-tab').classList.contains('hidden')) {
|
if (!document.getElementById('dashboard-tab').classList.contains('hidden')) {
|
||||||
loadStats();
|
loadStats();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<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"]</title>
|
<title>@ViewData["Title"]</title>
|
||||||
<script type="importmap"></script>
|
<script type="importmap"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@@microsoft/signalr@8.0.0/dist/browser/signalr.min.js"></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" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using EonaCat.Logger.LogServer.Hubs;
|
||||||
using EonaCat.Logger.Server.Data;
|
using EonaCat.Logger.Server.Data;
|
||||||
using EonaCat.Logger.Server.Services;
|
using EonaCat.Logger.Server.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -22,6 +23,8 @@ builder.Services.AddCors(options =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
@@ -43,5 +46,6 @@ app.UseCors();
|
|||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapRazorPages();
|
app.MapRazorPages();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
app.MapHub<LogHub>("/logHub");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -7,7 +7,7 @@ public interface ILogService
|
|||||||
Task AddLogsAsync(List<LogEntry> entries);
|
Task AddLogsAsync(List<LogEntry> entries);
|
||||||
Task<LogEntry?> GetLogByIdAsync(string id);
|
Task<LogEntry?> GetLogByIdAsync(string id);
|
||||||
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, bool skipCache);
|
||||||
Task<bool> ValidateApiKeyAsync(string apiKey);
|
Task<bool> ValidateApiKeyAsync(string apiKey);
|
||||||
Task<List<string>> GetApplicationsAsync();
|
Task<List<string>> GetApplicationsAsync();
|
||||||
Task<List<string>> GetEnvironmentsAsync();
|
Task<List<string>> GetEnvironmentsAsync();
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using EonaCat.Logger.Server.Data;
|
using EonaCat.Logger.LogServer.Hubs;
|
||||||
|
using EonaCat.Logger.Server.Data;
|
||||||
using EonaCat.Logger.Server.Models;
|
using EonaCat.Logger.Server.Models;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace EonaCat.Logger.Server.Services;
|
namespace EonaCat.Logger.Server.Services;
|
||||||
@@ -10,18 +13,55 @@ public class LogService : ILogService
|
|||||||
{
|
{
|
||||||
private readonly LoggerDbContext _context;
|
private readonly LoggerDbContext _context;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly IHubContext<LogHub> _hub;
|
||||||
|
|
||||||
public LogService(LoggerDbContext context, IMemoryCache cache)
|
public LogService(LoggerDbContext context, IHubContext<LogHub> hub, IMemoryCache cache)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_hub = hub;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddLogsAsync(List<LogEntry> entries)
|
public async Task AddLogsAsync(List<LogEntry> entries)
|
||||||
{
|
{
|
||||||
|
if (entries == null || entries.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Unique ids
|
||||||
|
foreach (var log in entries)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(log.Id) || _context.LogEntries.Any(e => e.Id == log.Id))
|
||||||
|
{
|
||||||
|
log.Id = Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save logs
|
||||||
await _context.LogEntries.AddRangeAsync(entries);
|
await _context.LogEntries.AddRangeAsync(entries);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Invalidate stats cache
|
||||||
_cache.Remove("stats");
|
_cache.Remove("stats");
|
||||||
|
|
||||||
|
// Generate payload for SignalR
|
||||||
|
var payload = entries.Select(log => new
|
||||||
|
{
|
||||||
|
id = log.Id,
|
||||||
|
level = log.Level,
|
||||||
|
message = log.Message,
|
||||||
|
applicationName = log.ApplicationName,
|
||||||
|
environment = log.Environment,
|
||||||
|
timestamp = log.Timestamp,
|
||||||
|
machineName = log.MachineName,
|
||||||
|
correlationId = log.CorrelationId
|
||||||
|
});
|
||||||
|
|
||||||
|
// // Send via SignalR
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
_hub.Clients.All.SendAsync("NewLogBatch", payload)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogEntry?> GetLogByIdAsync(string id)
|
public async Task<LogEntry?> GetLogByIdAsync(string id)
|
||||||
@@ -87,10 +127,11 @@ 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, bool skipCache = false)
|
||||||
{
|
{
|
||||||
var cacheKey = $"stats_{app}_{env}";
|
var cacheKey = $"stats_{app}_{env}";
|
||||||
if (_cache.TryGetValue(cacheKey, out Dictionary<string, object>? cachedStats))
|
|
||||||
|
if (!skipCache && _cache.TryGetValue(cacheKey, out Dictionary<string, object>? cachedStats))
|
||||||
return cachedStats!;
|
return cachedStats!;
|
||||||
|
|
||||||
var query = _context.LogEntries.AsQueryable();
|
var query = _context.LogEntries.AsQueryable();
|
||||||
@@ -101,43 +142,71 @@ public class LogService : ILogService
|
|||||||
if (!string.IsNullOrEmpty(env))
|
if (!string.IsNullOrEmpty(env))
|
||||||
query = query.Where(l => l.Environment == env);
|
query = query.Where(l => l.Environment == env);
|
||||||
|
|
||||||
var last24Hours = DateTime.UtcNow.AddHours(-24);
|
var now = DateTime.UtcNow;
|
||||||
var lastHour = DateTime.UtcNow.AddHours(-1);
|
var last24Hours = now.AddHours(-24);
|
||||||
|
var lastHour = now.AddHours(-1);
|
||||||
|
|
||||||
var stats = new Dictionary<string, object>
|
var counts = await query
|
||||||
|
.GroupBy(l => 1)
|
||||||
|
.Select(g => new
|
||||||
{
|
{
|
||||||
["totalLogs"] = await query.CountAsync(),
|
Total = g.Count(),
|
||||||
["last24Hours"] = await query.Where(l => l.Timestamp >= last24Hours).CountAsync(),
|
Last24Hours = g.Count(l => l.Timestamp >= last24Hours),
|
||||||
["lastHour"] = await query.Where(l => l.Timestamp >= lastHour).CountAsync(),
|
LastHour = g.Count(l => l.Timestamp >= lastHour),
|
||||||
["errorCount"] = await query.Where(l => l.Level >= 4).CountAsync(),
|
ErrorCount = g.Count(l => l.Level >= 4),
|
||||||
["warningCount"] = await query.Where(l => l.Level == 3).CountAsync(),
|
WarningCount = g.Count(l => l.Level == 3),
|
||||||
["criticalCount"] = await query.Where(l => l.Level == 5).CountAsync(),
|
CriticalCount = g.Count(l => l.Level == 5),
|
||||||
["securityCount"] = await query.Where(l => l.Level == 6).CountAsync(),
|
SecurityCount = g.Count(l => l.Level == 6)
|
||||||
["applications"] = await _context.LogEntries.Select(l => l.ApplicationName).Distinct().CountAsync(),
|
})
|
||||||
["byLevel"] = await query
|
.FirstOrDefaultAsync() ?? new { Total = 0, Last24Hours = 0, LastHour = 0, ErrorCount = 0, WarningCount = 0, CriticalCount = 0, SecurityCount = 0 };
|
||||||
|
|
||||||
|
var applicationsCount = await _context.LogEntries
|
||||||
|
.Select(l => l.ApplicationName)
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
var 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(),
|
.ToListAsync();
|
||||||
["topErrors"] = await query
|
|
||||||
|
var topErrors = await query
|
||||||
.Where(l => l.Level >= 4 && l.Timestamp >= last24Hours)
|
.Where(l => l.Level >= 4 && l.Timestamp >= last24Hours)
|
||||||
.GroupBy(l => l.Message)
|
.GroupBy(l => l.Message)
|
||||||
.Select(g => new { Message = g.Key, Count = g.Count() })
|
.Select(g => new { Message = g.Key, Count = g.Count() })
|
||||||
.OrderByDescending(x => x.Count)
|
.OrderByDescending(x => x.Count)
|
||||||
.Take(5)
|
.Take(5)
|
||||||
.ToListAsync(),
|
.ToListAsync();
|
||||||
["topApplications"] = await query
|
|
||||||
|
var topApplications = await query
|
||||||
.Where(l => l.Timestamp >= last24Hours)
|
.Where(l => l.Timestamp >= last24Hours)
|
||||||
.GroupBy(l => l.ApplicationName)
|
.GroupBy(l => l.ApplicationName)
|
||||||
.Select(g => new { Application = g.Key, Count = g.Count() })
|
.Select(g => new { Application = g.Key, Count = g.Count() })
|
||||||
.OrderByDescending(x => x.Count)
|
.OrderByDescending(x => x.Count)
|
||||||
.Take(5)
|
.Take(5)
|
||||||
.ToListAsync()
|
.ToListAsync();
|
||||||
|
|
||||||
|
var stats = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["totalLogs"] = counts.Total,
|
||||||
|
["last24Hours"] = counts.Last24Hours,
|
||||||
|
["lastHour"] = counts.LastHour,
|
||||||
|
["errorCount"] = counts.ErrorCount,
|
||||||
|
["warningCount"] = counts.WarningCount,
|
||||||
|
["criticalCount"] = counts.CriticalCount,
|
||||||
|
["securityCount"] = counts.SecurityCount,
|
||||||
|
["applications"] = applicationsCount,
|
||||||
|
["byLevel"] = byLevel,
|
||||||
|
["topErrors"] = topErrors,
|
||||||
|
["topApplications"] = topApplications
|
||||||
};
|
};
|
||||||
|
|
||||||
_cache.Set(cacheKey, stats, TimeSpan.FromSeconds(30));
|
_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.AnyAsync(a => a.ApiKey == apiKey && a.IsActive);
|
return await _context.Applications.AnyAsync(a => a.ApiKey == apiKey && a.IsActive);
|
||||||
|
|||||||
Reference in New Issue
Block a user