Files
OTSSignsOrchestrator/OTSSignsOrchestrator/API/LogsController.cs

150 lines
4.7 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using OTSSignsOrchestrator.Configuration;
using OTSSignsOrchestrator.Data;
namespace OTSSignsOrchestrator.API;
[ApiController]
[Route("api/admin/[controller]")]
[Authorize(Roles = AppConstants.AdminRole)]
public class LogsController : ControllerBase
{
private readonly FileLoggingOptions _loggingOptions;
private readonly IWebHostEnvironment _env;
private readonly ILogger<LogsController> _logger;
private readonly XiboContext _db;
public LogsController(
IOptions<FileLoggingOptions> loggingOptions,
IWebHostEnvironment env,
ILogger<LogsController> logger,
XiboContext db)
{
_loggingOptions = loggingOptions.Value;
_env = env;
_logger = logger;
_db = db;
}
/// <summary>
/// Tail recent log lines with optional filters.
/// </summary>
[HttpGet]
public IActionResult GetLogs([FromQuery] int lines = 100, [FromQuery] string? filter = null, [FromQuery] string? level = null)
{
var logPath = _loggingOptions.Path;
if (!Path.IsPathRooted(logPath))
logPath = Path.Combine(_env.ContentRootPath, logPath);
if (!Directory.Exists(logPath))
return Ok(new { lines = Array.Empty<string>(), path = logPath, message = "Log directory not found." });
// Find latest log file
var logFiles = Directory.GetFiles(logPath, "app-*.log")
.OrderByDescending(f => f)
.ToList();
if (logFiles.Count == 0)
return Ok(new { lines = Array.Empty<string>(), path = logPath, message = "No log files found." });
var latestFile = logFiles[0];
var allLines = ReadLastLines(latestFile, lines * 2); // Read extra for filtering
// Apply filters
if (!string.IsNullOrWhiteSpace(level))
{
allLines = allLines.Where(l =>
l.Contains($"[{level.ToUpperInvariant().PadRight(3)}]", StringComparison.OrdinalIgnoreCase) ||
l.Contains($"[{level}]", StringComparison.OrdinalIgnoreCase)).ToList();
}
if (!string.IsNullOrWhiteSpace(filter))
{
allLines = allLines.Where(l =>
l.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToList();
}
var result = allLines.TakeLast(lines).ToList();
var fileInfo = new FileInfo(latestFile);
return Ok(new
{
lines = result,
path = latestFile,
sizeBytes = fileInfo.Length,
lastModified = fileInfo.LastWriteTimeUtc
});
}
/// <summary>
/// Download the latest log file.
/// </summary>
[HttpGet("download")]
public IActionResult DownloadLog()
{
var logPath = _loggingOptions.Path;
if (!Path.IsPathRooted(logPath))
logPath = Path.Combine(_env.ContentRootPath, logPath);
var logFiles = Directory.GetFiles(logPath, "app-*.log")
.OrderByDescending(f => f)
.ToList();
if (logFiles.Count == 0)
return NotFound("No log files found.");
var latestFile = logFiles[0];
var stream = new FileStream(latestFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return File(stream, "text/plain", Path.GetFileName(latestFile));
}
/// <summary>
/// Get recent operation logs from the database.
/// </summary>
[HttpGet("operations")]
public async Task<IActionResult> GetOperations([FromQuery] int count = 50)
{
var ops = await _db.OperationLogs
.Include(o => o.Instance)
.OrderByDescending(o => o.Timestamp)
.Take(Math.Min(count, 200))
.Select(o => new
{
o.Id,
o.Operation,
o.Status,
o.Message,
o.Timestamp,
o.DurationMs,
o.UserId,
StackName = o.Instance != null ? o.Instance.StackName : null
})
.ToListAsync();
return Ok(ops);
}
private static List<string> ReadLastLines(string filePath, int lineCount)
{
try
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
var lines = new List<string>();
string? line;
while ((line = reader.ReadLine()) != null)
{
lines.Add(line);
}
return lines.TakeLast(lineCount).ToList();
}
catch
{
return new List<string>();
}
}
}