using EonaCat.LogStack.Status.Models; using EonaCat.LogStack.Status.Services; using System.Net.Sockets; using System.Text; using System.Text.Json; using System.Threading.Channels; public class SyslogUdpService : BackgroundService { private readonly ILogger _logger; private readonly IngestionService _ingestionService; private readonly int _port; private readonly Channel _channel; private readonly IServiceScopeFactory _scopeFactory; public static bool IsRunning { get; private set; } public SyslogUdpService(ILogger logger, IServiceScopeFactory scopeFactory, IConfiguration config) { _logger = logger; _scopeFactory = scopeFactory; _port = config.GetValue("Syslog:Port", 514); _channel = Channel.CreateBounded(new BoundedChannelOptions(10_000) { FullMode = BoundedChannelFullMode.DropOldest }); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var receiverTask = ReceiveLoop(stoppingToken); var processorTask = ProcessLoop(stoppingToken); await Task.WhenAll(receiverTask, processorTask); } private async Task ReceiveLoop(CancellationToken token) { using var udpClient = new UdpClient(_port); udpClient.Client.ReceiveBufferSize = 4 * 1024 * 1024; _logger.LogInformation("Syslog UDP server listening on port {Port}", _port); IsRunning = true; while (!token.IsCancellationRequested) { try { var result = await udpClient.ReceiveAsync(token); var message = Encoding.UTF8.GetString(result.Buffer); var remoteIp = result.RemoteEndPoint.Address.ToString(); var entry = ParseMessage(message, remoteIp); if (entry != null) { await _channel.Writer.WriteAsync(entry, token); } } catch (OperationCanceledException) { break; } catch (Exception ex) { _logger.LogError(ex, "Receive loop error"); } } } private async Task ProcessLoop(CancellationToken token) { var batch = new List(100); while (await _channel.Reader.WaitToReadAsync(token)) { while (_channel.Reader.TryRead(out var entry)) { batch.Add(entry); if (batch.Count >= 100) { await Flush(batch); } } if (batch.Count > 0) { await Flush(batch); } } } private async Task Flush(List batch) { try { using var scope = _scopeFactory.CreateScope(); var ingestionService = scope.ServiceProvider.GetRequiredService(); await ingestionService.IngestBatchAsync(batch.ToArray()); } catch (Exception ex) { _logger.LogError(ex, "Batch ingestion failed"); } finally { batch.Clear(); } } private LogEntry ParseMessage(string rawMessage, string remoteIp) { try { if (IsJson(rawMessage)) return ParseJson(rawMessage, remoteIp); return ParseSyslogAdvanced(rawMessage, remoteIp); } catch (Exception ex) { _logger.LogWarning(ex, "Parsing failed"); return new LogEntry { Source = "Syslog.Unknown", Level = "Info", Message = rawMessage, Host = remoteIp, Timestamp = DateTime.UtcNow }; } } private bool IsJson(string input) { input = input.TrimStart(); return input.StartsWith("{") || input.StartsWith("["); } private LogEntry ParseJson(string json, string remoteIp) { using var doc = JsonDocument.Parse(json); var root = doc.RootElement; string Get(string name) => root.TryGetProperty(name, out var val) ? val.ToString() : null; return new LogEntry { Source = Get("source") ?? "Custom.Json", Level = MapLevel(Get("level")), Message = Get("message"), Exception = Get("exception"), Host = Get("host") ?? remoteIp, TraceId = Get("traceId"), Properties = json, Timestamp = DateTime.TryParse(Get("timestamp"), out var dt) ? dt : DateTime.UtcNow }; } private LogEntry ParseSyslogAdvanced(string message, string remoteIp) { var entry = new LogEntry { Source = "Syslog", Host = remoteIp, Message = message, Timestamp = DateTime.UtcNow, Level = "Info" }; try { // PRI parsing if (message.StartsWith("<")) { var end = message.IndexOf('>'); if (end > 0) { var pri = int.Parse(message[1..end]); var severity = pri % 8; entry.Level = MapSyslogSeverity(severity); message = message[(end + 1)..].Trim(); } } // Try RFC5424 parsing var parts = message.Split(' ', 7, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 7 && int.TryParse(parts[0], out _)) { entry.Timestamp = DateTime.TryParse(parts[1], out var ts) ? ts : entry.Timestamp; entry.Host = parts[2]; entry.Source = parts[3]; entry.Message = parts[6]; } else { entry.Message = message; } } catch { // fallback } return entry; } private string MapSyslogSeverity(int severity) => severity switch { 0 or 1 => "Fatal", 2 or 3 => "Error", 4 => "Warning", 5 or 6 => "Info", 7 => "Debug", _ => "Info" }; private string MapLevel(string level) => level?.ToLower() switch { "trace" => "Debug", "debug" => "Debug", "info" => "Info", "warn" or "warning" => "Warning", "error" => "Error", "fatal" => "Fatal", _ => "Info" }; }