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 GetSummary() { var isAdmin = HttpContext.Session.GetString("IsAdmin") == "true"; var stats = await _monitorService.GetStatsAsync(isAdmin); return Ok(stats); } [HttpGet("monitors/{id}/check")] public async Task 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 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 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 IngestBatch([FromBody] List 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 IngestEonaCat([FromBody] object[] events) { var entries = new List(); foreach (var evtObj in events) { // Use System.Text.Json to parse the dictionary dynamically var json = JsonSerializer.Serialize(evtObj); var dict = JsonSerializer.Deserialize>(json)!; var logEntry = new LogEntry { Source = dict.TryGetValue("properties", out var props) && props.ValueKind == JsonValueKind.Object && props.Deserialize>().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 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(); 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 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? 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? 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? 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? Properties { get; set; } }