diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index c1921d8..360b06a 100644 --- a/EonaCat.Logger/EonaCat.Logger.csproj +++ b/EonaCat.Logger/EonaCat.Logger.csproj @@ -3,7 +3,7 @@ .netstandard2.1; net6.0; net7.0; net8.0; net4.8; icon.ico latest - 1.3.4 + 1.3.5 EonaCat (Jeroen Saey) true EonaCat (Jeroen Saey) @@ -24,7 +24,7 @@ - 1.3.4+{chash:10}.{c:ymd} + 1.3.5+{chash:10}.{c:ymd} true true v[0-9]* diff --git a/EonaCat.Logger/GrayLog/GrayLogServer.cs b/EonaCat.Logger/GrayLog/GrayLogServer.cs index 697a42c..3f134ce 100644 --- a/EonaCat.Logger/GrayLog/GrayLogServer.cs +++ b/EonaCat.Logger/GrayLog/GrayLogServer.cs @@ -75,8 +75,10 @@ public class GrayLogServer /// /// IP:port of the server. /// - public string IpPort => _Hostname + ":" + _Port; - + public string IpPort => _Hostname + ":" + _Port; + + public bool SupportsTcp { get; set; } + private void SetUdp() { Udp = null; diff --git a/EonaCat.Logger/Managers/LogHelper.cs b/EonaCat.Logger/Managers/LogHelper.cs index 8cbced9..fa23b5d 100644 --- a/EonaCat.Logger/Managers/LogHelper.cs +++ b/EonaCat.Logger/Managers/LogHelper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using EonaCat.Json; @@ -144,33 +145,36 @@ internal static class LogHelper public static async Task SendToSplunkServersAsync(LoggerSettings settings, SplunkPayload splunkPayload, bool sendToSplunkServer) { - if (settings == null || !sendToSplunkServer || splunkPayload == null) return; - - foreach (var splunkServer in settings.SplunkServers ?? Enumerable.Empty()) + if (settings == null || !sendToSplunkServer || splunkPayload == null) { - if (!splunkServer.HasHecUrl || !splunkServer.HasHecToken) - { - OnException?.Invoke(null, new ErrorMessage { Message = $"Invalid Splunk server configuration for '{splunkServer.SplunkHecUrl}'" }); - continue; - } - - try - { - var response = await splunkServer.SendAsync(splunkPayload); - if (!response.IsSuccessStatusCode) - { - OnException?.Invoke(null, new ErrorMessage { Message = $"Failed to send log to Splunk '{splunkServer.SplunkHecUrl}'. Status code: {response.StatusCode}" }); - } - } - catch (Exception ex) - { - OnException?.Invoke(null, new ErrorMessage { Exception = ex, Message = $"Error logging to Splunk '{splunkServer.SplunkHecUrl}': {ex.Message}" }); - } + return; } + + var tasks = settings.SplunkServers? + .Where(splunkServer => splunkServer.HasHecUrl && splunkServer.HasHecToken) + .Select(async splunkServer => + { + try + { + var response = await splunkServer.SendAsync(splunkPayload); + if (!response.IsSuccessStatusCode) + { + LogError($"Failed to send log to Splunk '{splunkServer.SplunkHecUrl}'. Status code: {response.StatusCode}"); + } + } + catch (Exception ex) + { + LogError($"Error logging to Splunk '{splunkServer.SplunkHecUrl}': {ex.Message}", ex); + } + }) ?? new List(); + + await Task.WhenAll(tasks); } - public static async Task SendToSplunkServersAsync(LoggerSettings settings, string logType, string message, - bool sendToSplunkServer) + /// + /// Overload for sending a simple log message to Splunk. + /// + public static async Task SendToSplunkServersAsync(LoggerSettings settings, string logType, string message, bool sendToSplunkServer) { if (settings == null || !sendToSplunkServer || string.IsNullOrWhiteSpace(message)) { @@ -187,86 +191,169 @@ internal static class LogHelper await SendToSplunkServersAsync(settings, splunkPayload, sendToSplunkServer); } + /// + /// Logs an error using the OnException event. + /// + private static void LogError(string message, Exception ex = null) + { + OnException?.Invoke(null, new ErrorMessage { Message = message, Exception = ex }); + } + + internal static async Task SendToGrayLogServersAsync(LoggerSettings settings, string message, ELogType logLevel, - string facility, string source, bool sendToGrayLogServer, string version = "1.1") + string facility, string source, bool sendToGrayLogServer, string version = "1.1") { if (settings == null || !sendToGrayLogServer || string.IsNullOrWhiteSpace(message)) { return; } - foreach (var grayLogServer in settings.GrayLogServers ?? new List { new("127.0.0.1") }) - { - try - { - var gelfMessage = new - { - version, - host = MachineName, - short_message = message, - level = logLevel.ToGrayLogLevel(), - facility, - source, - timestamp = DateTime.UtcNow.ToUnixTimestamp() - }; + const int MaxUdpPacketSize = 4096; - var messageBytes = Encoding.UTF8.GetBytes(JsonHelper.ToJson(gelfMessage)); - await grayLogServer.Udp.SendAsync(messageBytes, messageBytes.Length); - } - catch (Exception exception) + var gelfMessage = new + { + version, + host = MachineName, + short_message = message, + level = logLevel.ToGrayLogLevel(), + facility, + source, + timestamp = DateTime.UtcNow.ToUnixTimestamp() + }; + + var messageBytes = Encoding.UTF8.GetBytes(JsonHelper.ToJson(gelfMessage)); + + var tasks = settings.GrayLogServers? + .Where(server => !string.IsNullOrWhiteSpace(server.Hostname) && server.Port >= 0) + .Select(async grayLogServer => { - OnException?.Invoke(null, - new ErrorMessage + try + { + if (messageBytes.Length <= MaxUdpPacketSize) { - Exception = exception, - Message = - $"Error while logging to GrayLog Server '{grayLogServer.Hostname}': {exception.Message}" + // Send via UDP (single packet) + await grayLogServer.Udp.SendAsync(messageBytes, messageBytes.Length); + } + else if (grayLogServer.SupportsTcp) + { + // Send via TCP if supported + await SendViaTcpAsync(grayLogServer, messageBytes); + } + else + { + // Chunk large messages for UDP + await SendUdpInChunksAsync(grayLogServer, messageBytes, MaxUdpPacketSize); + } + } + catch (Exception ex) + { + OnException?.Invoke(null, new ErrorMessage + { + Exception = ex, + Message = $"Error logging to GrayLog Server '{grayLogServer.Hostname}': {ex.Message}" }); - } + } + }) ?? new List(); + + await Task.WhenAll(tasks); + } + + + /// + /// Sends a message via TCP to a GrayLog server. + /// + private static async Task SendViaTcpAsync(GrayLogServer server, byte[] data) + { + using var tcpClient = new TcpClient(); + await tcpClient.ConnectAsync(server.Hostname, server.Port); + using var stream = tcpClient.GetStream(); + await stream.WriteAsync(data, 0, data.Length); + await stream.FlushAsync(); + } + + /// + /// Sends large messages in chunks over UDP. + /// + private static async Task SendUdpInChunksAsync(GrayLogServer server, byte[] data, int chunkSize) + { + for (int i = 0; i < data.Length; i += chunkSize) + { + var chunk = data.Skip(i).Take(chunkSize).ToArray(); + await server.Udp.SendAsync(chunk, chunk.Length); } } - internal static async Task SendToSysLogServersAsync(LoggerSettings settings, string message, - bool sendToSyslogServers) + internal static async Task SendToSysLogServersAsync(LoggerSettings settings, string message, bool sendToSyslogServers) { if (settings == null || !sendToSyslogServers || string.IsNullOrWhiteSpace(message)) { return; } - foreach (var server in settings.SysLogServers ?? new List { new("127.0.0.1") }) - { - try - { - if (string.IsNullOrWhiteSpace(server.Hostname)) - { - OnException?.Invoke(null, - new ErrorMessage { Message = "Server hostname not specified, skipping SysLog Server" }); - continue; - } + const int MaxUdpPacketSize = 4096; - if (server.Port < 0) - { - OnException?.Invoke(null, - new ErrorMessage { Message = "Server port must be zero or greater, skipping SysLog Server" }); - continue; - } - - var data = Encoding.UTF8.GetBytes(message); - await server.Udp.SendAsync(data, data.Length); - } - catch (Exception exception) + var tasks = settings.SysLogServers? + .Where(server => !string.IsNullOrWhiteSpace(server.Hostname) && server.Port >= 0) + .Select(async server => { - OnException?.Invoke(null, - new ErrorMessage + try + { + var data = Encoding.UTF8.GetBytes(message); + + if (data.Length <= MaxUdpPacketSize) { - Exception = exception, - Message = $"Error while logging to SysLog Server '{server.Hostname}': {exception.Message}" + // Send via UDP (single packet) + await server.Udp.SendAsync(data, data.Length); + } + else if (server.SupportsTcp) + { + // Send via TCP if supported + await SendViaTcpAsync(server, data); + } + else + { + // Chunk large messages for UDP + await SendUdpInChunksAsync(server, data, MaxUdpPacketSize); + } + } + catch (Exception ex) + { + OnException?.Invoke(null, new ErrorMessage + { + Exception = ex, + Message = $"Error logging to SysLog Server '{server.Hostname}': {ex.Message}" }); - } + } + }) ?? new List(); + + await Task.WhenAll(tasks); + } + + /// + /// Sends a message via TCP to a syslog server. + /// + private static async Task SendViaTcpAsync(SyslogServer server, byte[] data) + { + using var tcpClient = new TcpClient(); + await tcpClient.ConnectAsync(server.Hostname, server.Port); + using var stream = tcpClient.GetStream(); + await stream.WriteAsync(data, 0, data.Length); + await stream.FlushAsync(); + } + + /// + /// Sends large messages in chunks over UDP. + /// + private static async Task SendUdpInChunksAsync(SyslogServer server, byte[] data, int chunkSize) + { + for (int i = 0; i < data.Length; i += chunkSize) + { + var chunk = data.Skip(i).Take(chunkSize).ToArray(); + await server.Udp.SendAsync(chunk, chunk.Length); } } + internal static string GetStartupMessage() { return $"{DllInfo.ApplicationName} started."; diff --git a/EonaCat.Logger/Syslog/SyslogServer.cs b/EonaCat.Logger/Syslog/SyslogServer.cs index e725a9c..824ff1b 100644 --- a/EonaCat.Logger/Syslog/SyslogServer.cs +++ b/EonaCat.Logger/Syslog/SyslogServer.cs @@ -75,8 +75,10 @@ public class SyslogServer /// /// IP:port of the server. /// - public string IpPort => _Hostname + ":" + _Port; - + public string IpPort => _Hostname + ":" + _Port; + + public bool SupportsTcp { get; set; } + private void SetUdp() { Udp = null;