Added EonaCat.LogStack.Status

Updated EonaCat.LogStack.LogClient to support EonaCat.LogStack.Status
This commit is contained in:
2026-03-12 21:15:33 +01:00
parent 776cc624bd
commit 977374ce02
41 changed files with 3412 additions and 180 deletions

View File

@@ -0,0 +1,251 @@
using EonaCat.LogStack.Status.Data;
using EonaCat.LogStack.Status.Models;
using EonaCat.LogStack.Status.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace EonaCat.LogStack.Status.Controllers;
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
[ApiController]
[Route("api")]
public class ApiController : ControllerBase
{
private readonly DatabaseContext _database;
private readonly MonitoringService _monitorService;
private readonly IngestionService _ingestionService;
public ApiController(DatabaseContext database, MonitoringService monitorService, IngestionService ingestionService)
{
_database = database;
_monitorService = monitorService;
_ingestionService = ingestionService;
}
[HttpGet("status/summary")]
public async Task<IActionResult> GetSummary()
{
var isAdmin = HttpContext.Session.GetString("IsAdmin") == "true";
var stats = await _monitorService.GetStatsAsync(isAdmin);
return Ok(stats);
}
[HttpGet("monitors/{id}/check")]
public async Task<IActionResult> CheckMonitor(int id)
{
var monitor = await _database.Monitors.FindAsync(id);
if (monitor == null)
{
return NotFound();
}
var check = await _monitorService.CheckMonitorAsync(monitor);
return Ok(check);
}
[HttpGet("monitors")]
public async Task<IActionResult> GetMonitors()
{
var isAdmin = HttpContext.Session.GetString("IsAdmin") == "true";
var query = _database.Monitors.Where(m => m.IsActive);
if (!isAdmin)
{
query = query.Where(m => m.IsPublic);
}
return Ok(await query.ToListAsync());
}
[HttpPost("logs/ingest")]
public async Task<IActionResult> IngestLog([FromBody] LogEntry entry)
{
if (string.IsNullOrWhiteSpace(entry.Message))
{
return BadRequest("message required");
}
entry.Level = (entry.Level ?? "info").ToLower();
entry.Timestamp = DateTime.UtcNow;
await _ingestionService.IngestAsync(entry);
return Ok(new { success = true });
}
[HttpPost("logs/batch")]
public async Task<IActionResult> IngestBatch([FromBody] List<LogEntry> entries)
{
if (entries == null || !entries.Any())
{
return BadRequest("entries required");
}
foreach (var entry in entries)
{
entry.Level = (entry.Level ?? "info").ToLower();
if (entry.Timestamp == default)
{
entry.Timestamp = DateTime.UtcNow;
}
}
await _ingestionService.IngestBatchAsync(entries);
return Ok(new { success = true, count = entries.Count });
}
[HttpPost("logs/eonacat")]
public async Task<IActionResult> IngestEonaCat([FromBody] object[] events)
{
var entries = new List<LogEntry>();
foreach (var evtObj in events)
{
// Use System.Text.Json to parse the dictionary dynamically
var json = JsonSerializer.Serialize(evtObj);
var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json)!;
var logEntry = new LogEntry
{
Source = dict.TryGetValue("properties", out var props) &&
props.ValueKind == JsonValueKind.Object &&
props.Deserialize<Dictionary<string, object?>>().TryGetValue("Application", out var appObj)
? appObj?.ToString() ?? "EonaCat.LogStack"
: "EonaCat.LogStack",
Level = dict.TryGetValue("level", out var level) ? level.GetString() ?? "Info" : "Info",
Message = dict.TryGetValue("message", out var msg) ? msg.GetString() ?? "" : "",
Exception = dict.TryGetValue("exception", out var ex) ? ex.ToString() : null,
Host = dict.TryGetValue("host", out var host) ? host.GetString() : null,
TraceId = dict.TryGetValue("traceId", out var traceId) ? traceId.GetString() : null,
Properties = dict.TryGetValue("properties", out var properties) ? properties.GetRawText() : null,
Timestamp = dict.TryGetValue("timestamp", out var ts) && DateTime.TryParse(ts.GetString(), out var dt)
? dt
: DateTime.UtcNow
};
logEntry.Level = MapEonaCatLevel(logEntry.Level);
entries.Add(logEntry);
}
if (entries.Any())
{
await _ingestionService.IngestBatchAsync(entries);
}
return Ok(new { success = true, count = entries.Count });
}
[HttpPost("logs/serilog")]
public async Task<IActionResult> IngestSerilog([FromBody] SerilogPayload payload)
{
var entries = payload.Events?.Select(e => new LogEntry
{
Source = e.Properties?.TryGetValue("Application", out var app) == true ? app?.ToString() ?? "serilog" : "serilog",
Level = MapSerilogLevel(e.Level),
Message = e.RenderedMessage ?? e.MessageTemplate ?? "",
Exception = e.Exception,
Properties = e.Properties != null ? System.Text.Json.JsonSerializer.Serialize(e.Properties) : null,
Timestamp = e.Timestamp == default ? DateTime.UtcNow : e.Timestamp
}).ToList() ?? new List<LogEntry>();
if (entries.Any())
{
await _ingestionService.IngestBatchAsync(entries);
}
return Ok(new { success = true, count = entries.Count });
}
private static string MapSerilogLevel(string? l) => l?.ToLower() switch {
"verbose" or "debug" => "debug",
"information" => "info",
"warning" => "warn",
"error" => "error",
"fatal" => "critical",
_ => "info"
};
private static string MapEonaCatLevel(string? l) => l?.ToLower() switch
{
"trace" or "debug" => "debug",
"information" => "info",
"warning" => "warn",
"error" => "error",
"critical" => "critical",
_ => "info"
};
[HttpGet("logs")]
public async Task<IActionResult> QueryLogs([FromQuery] string? level, [FromQuery] string? source, [FromQuery] string? search, [FromQuery] int page = 1, [FromQuery] int pageSize = 100)
{
if (HttpContext.Session.GetString("IsAdmin") != "true")
{
return Unauthorized();
}
var query = _database.Logs.AsQueryable();
if (!string.IsNullOrWhiteSpace(level))
{
query = query.Where(x => x.Level == level.ToLower());
}
if (!string.IsNullOrWhiteSpace(source))
{
query = query.Where(x => x.Source == source);
}
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(x => x.Message.Contains(search));
}
var total = await query.CountAsync();
var entries = await query.OrderByDescending(x => x.Timestamp).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return Ok(new { total, page, pageSize, entries });
}
}
public class EonaCatPayLoad
{
public List<EonaCatLogEvent>? Events { get; set; }
}
public class EonaCatLogEvent
{
public string Timestamp { get; set; } = default!;
public string Level { get; set; } = default!;
public string Message { get; set; } = default!;
public string Category { get; set; } = default!;
public int ThreadId { get; set; }
public string? TraceId { get; set; }
public string? SpanId { get; set; }
public ExceptionDto? Exception { get; set; }
public Dictionary<string, object?>? Properties { get; set; }
}
public class ExceptionDto
{
public string Type { get; set; } = default!;
public string Message { get; set; } = default!;
public string? StackTrace { get; set; }
}
public class SerilogPayload
{
public List<SerilogEvent>? Events { get; set; }
}
public class SerilogEvent
{
public DateTime Timestamp { get; set; }
public string? Level { get; set; }
public string? MessageTemplate { get; set; }
public string? RenderedMessage { get; set; }
public string? Exception { get; set; }
public Dictionary<string, object?>? Properties { get; set; }
}