using Marco.Pms.Model.Logs; using Marco.Pms.Model.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Serilog.Context; using System.Text.Json; namespace Marco.Pms.Services.Controllers { [Authorize] [Route("api/[controller]")] [ApiController] public class LogController : ControllerBase { private readonly ILogger _logger; public LogController(ILogger logger) { _logger = logger; } [HttpPost] /// /// Receives a batch of log entries from mobile applications and processes them. /// Logs are enriched with context properties like UserAgent, Timestamp, and IpAddress. /// /// A list of LogStructure objects containing log details. /// An indicating the success of the operation. public IActionResult MobileLogging([FromBody] List logs) { // Check if logs are provided to avoid processing empty requests if (logs == null || !logs.Any()) { _logger.LogWarning("MobileLogging endpoint received an empty or null log list."); return BadRequest("No log entries provided."); } // Iterate through each log entry received from the client foreach (var log in logs) { object? detailsObject = null; // Attempt to deserialize the 'Details' string into a dynamic object // This allows structured logging of the details payload. if (!string.IsNullOrWhiteSpace(log.Details)) { try { // Use JsonDocument for potentially more efficient parsing if you just need the raw JSON or a dynamic object // For simply logging it as a structured property, object works. detailsObject = JsonSerializer.Deserialize>(log.Details); } catch (JsonException ex) { // Log a warning if deserialization fails, but continue processing other logs _logger.LogWarning("Failed to deserialize 'Details' for a log entry. Raw details: '{RawDetails}'. Error: {ErrorMessage}", log.Details, ex.Message); } } // Push common properties to the Serilog LogContext for the current log operation. // These properties will be attached to the log event itself. using (LogContext.PushProperty("UserAgent", log.UserAgent)) using (LogContext.PushProperty("Timestamp", log.TimeStamp)) using (LogContext.PushProperty("IpAddress", log.IpAddress)) using (LogContext.PushProperty("LogOrigin", "Mobile Application")) { // If details were successfully parsed, push them as a structured property. if (detailsObject != null) { using (LogContext.PushProperty("Details", detailsObject, true)) // 'true' for destructuring { LogByLevel(log.LogLevel, log.Message); } } else { // If details were null or failed to parse, log without the details property LogByLevel(log.LogLevel, log.Message); } } } // Return 200 OK indicating successful receipt of logs. // As logging is typically a "fire and forget" operation from the client's perspective, // we don't need to await individual log writes here. // The logger provider handles the actual writing asynchronously in the background. _logger.LogInformation("Successfully processed {LogCount} mobile log entries.", logs.Count); return Ok(ApiResponse.SuccessResponse(new { }, "Logs received successfully.", 200)); } /// /// Logs a message at the specified log level using the injected ILogger. /// /// The string representation of the log level (e.g., "info", "warning"). /// The log message. private void LogByLevel(string? logLevel, string? message) { // Default message if null or empty message = string.IsNullOrWhiteSpace(message) ? "No message provided." : message; // Use a switch expression or simple if/else for clarity based on log level switch (logLevel?.ToLowerInvariant()) // Use ToLowerInvariant for case-insensitive comparison { case "debug": _logger.LogDebug(message); break; case "info": case "information": // Common alias for info _logger.LogInformation(message); break; case "warn": case "warning": _logger.LogWarning(message); break; case "error": _logger.LogError(message); break; case "critical": _logger.LogCritical(message); break; default: // Log unknown levels as information to ensure they are captured _logger.LogInformation("[Unknown Level] {Message}", message); break; } } } }