Added EonaCat.LogStack.Status
Updated EonaCat.LogStack.LogClient to support EonaCat.LogStack.Status
This commit is contained in:
251
EonaCat.LogStack.Status/Controllers/ApiController.cs
Normal file
251
EonaCat.LogStack.Status/Controllers/ApiController.cs
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user