From ec32464c5b402ec1f66d9abb8745b790982d36ea Mon Sep 17 00:00:00 2001 From: EonaCat Date: Fri, 25 Jul 2025 21:30:24 +0200 Subject: [PATCH] Updated --- EonaCat.Logger/Servers/GrayLog/Graylog.cs | 1 + EonaCat.Logger/Servers/Splunk/Splunk.cs | 252 ++++++------- EonaCat.Logger/Servers/Syslog/Syslog.cs | 351 +++++++++--------- EonaCat.Logger/Servers/Tcp/Tcp.cs | 160 ++++---- EonaCat.Logger/Servers/Udp/Udp.cs | 196 +++++----- .../Servers/Zabbix/API/ZabbixApi.cs | 207 +++++------ Testers/EonaCat.Logger.Test.Web/Program.cs | 4 - 7 files changed, 549 insertions(+), 622 deletions(-) diff --git a/EonaCat.Logger/Servers/GrayLog/Graylog.cs b/EonaCat.Logger/Servers/GrayLog/Graylog.cs index 39b5b76..9673a92 100644 --- a/EonaCat.Logger/Servers/GrayLog/Graylog.cs +++ b/EonaCat.Logger/Servers/GrayLog/Graylog.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net.Sockets; namespace EonaCat.Logger.Servers.GrayLog; + // 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. diff --git a/EonaCat.Logger/Servers/Splunk/Splunk.cs b/EonaCat.Logger/Servers/Splunk/Splunk.cs index 8e332d4..297c3a0 100644 --- a/EonaCat.Logger/Servers/Splunk/Splunk.cs +++ b/EonaCat.Logger/Servers/Splunk/Splunk.cs @@ -1,155 +1,159 @@ -using EonaCat.Json; - -namespace EonaCat.Logger.Servers.Splunk; - -using EonaCat.Logger.Servers.Splunk.Models; -// 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. - -/// -/// Splunk Server. -/// -using System; +using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using EonaCat.Json; +using EonaCat.Logger.Servers.Splunk.Models; -public class Splunk : IDisposable +namespace EonaCat.Logger.Servers.Splunk { - internal readonly object SendLock = new(); - private string _splunkHecUrl = "https://127.0.0.1:8088/services/collector/event"; - - public string SplunkHecToken { get; set; } = "splunk-hec-token"; - public HttpClientHandler SplunkClientHandler { get; private set; } - public HttpClient HttpClient { get; private set; } - public string Nickname { get; set; } - - public bool HasHecToken => !string.IsNullOrWhiteSpace(SplunkHecToken) && SplunkHecToken != "splunk-hec-token"; - public bool HasHecUrl => !string.IsNullOrWhiteSpace(SplunkHecUrl); - public bool IsHttpsHecUrl => HasHecUrl && _splunkHecUrl.ToLower().StartsWith("https"); - public bool IsLocalHost => HasHecUrl && (_splunkHecUrl.ToLower().Contains("127.0.0.1") || _splunkHecUrl.ToLower().Contains("localhost")); - /// - /// Splunk Server + /// Splunk Server. /// - /// - /// - /// - /// - /// - /// - public Splunk(string splunkHecUrl, string splunkHecToken, HttpClientHandler httpClientHandler = null, string nickName = null, List typesToLog = null) + public class Splunk : IDisposable { - SplunkHecUrl = splunkHecUrl ?? throw new ArgumentNullException(nameof(splunkHecUrl)); - SplunkHecToken = splunkHecToken ?? throw new ArgumentNullException(nameof(splunkHecToken)); + private string _splunkHecUrl = "https://127.0.0.1:8088/services/collector/event"; + private HttpClient _httpClient; + private HttpClientHandler _httpClientHandler; + public event EventHandler OnException; - Nickname = nickName ?? $"{splunkHecUrl}_{splunkHecToken}"; - SplunkClientHandler = httpClientHandler ?? new HttpClientHandler(); - TypesToLog = typesToLog; + public string SplunkHecToken { get; set; } = "splunk-hec-token"; + public string Nickname { get; set; } + public List TypesToLog { get; set; } - CreateHttpClient(); - } - - public string SplunkHecUrl - { - get => _splunkHecUrl; - set + public string SplunkHecUrl { - if (!string.IsNullOrWhiteSpace(value) && !value.ToLower().Contains("http")) + get => _splunkHecUrl; + set { - value = $"http://{value}"; + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(SplunkHecUrl)); + + _splunkHecUrl = value.StartsWith("http", StringComparison.OrdinalIgnoreCase) + ? value + : $"http://{value}"; + + RecreateHttpClient(); } - - _splunkHecUrl = value; - CreateHttpClient(); - } - } - - public List TypesToLog { get; set; } - - private void CreateHttpClient() - { - DisposeHttpClient(); - - SplunkClientHandler ??= new HttpClientHandler(); - HttpClient = new HttpClient(SplunkClientHandler) - { - BaseAddress = new Uri(SplunkHecUrl) - }; - - HttpClient.DefaultRequestHeaders.Add("Authorization", $"Splunk {SplunkHecToken}"); - } - - public void DisableSSLValidation() - { - DisposeHttpClient(); - - SplunkClientHandler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true - }; - - CreateHttpClient(); - } - - public async Task SendAsync(SplunkPayload splunkPayload, bool disableSplunkSSL = false) - { - if (splunkPayload == null) - { - return null; } - var eventObject = new - { - @event = splunkPayload.EventData, - sourcetype = splunkPayload.SourceType, - host = splunkPayload.Host - }; + public bool HasHecToken => !string.IsNullOrWhiteSpace(SplunkHecToken) && SplunkHecToken != "splunk-hec-token"; + public bool HasHecUrl => !string.IsNullOrWhiteSpace(_splunkHecUrl); + public bool IsHttpsHecUrl => _splunkHecUrl.StartsWith("https", StringComparison.OrdinalIgnoreCase); + public bool IsLocalHost => _splunkHecUrl.ToLower().Contains("127.0.0.1") || _splunkHecUrl.ToLower().Contains("localhost"); - if (disableSplunkSSL) - { - DisableSSLValidation(); - } - - if (HttpClient == null) + public Splunk(string splunkHecUrl, string splunkHecToken, HttpClientHandler handler = null, string nickName = null, List typesToLog = null) { + SplunkHecToken = splunkHecToken ?? throw new ArgumentNullException(nameof(splunkHecToken)); + SplunkHecUrl = splunkHecUrl ?? throw new ArgumentNullException(nameof(splunkHecUrl)); + Nickname = nickName ?? $"{splunkHecUrl}_{splunkHecToken}"; + TypesToLog = typesToLog; + _httpClientHandler = handler ?? new HttpClientHandler(); CreateHttpClient(); } - var eventJson = JsonHelper.ToJson(eventObject); - if (!HasHecToken) + private void CreateHttpClient() { + DisposeHttpClient(); + + _httpClientHandler ??= new HttpClientHandler(); + _httpClient = new HttpClient(_httpClientHandler) + { + BaseAddress = new Uri(SplunkHecUrl) + }; + + if (!_httpClient.DefaultRequestHeaders.Contains("Authorization")) + { + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Splunk {SplunkHecToken}"); + } + } + + private void RecreateHttpClient() + { + DisposeHttpClient(); + _httpClientHandler = new HttpClientHandler(); CreateHttpClient(); } - using var content = new StringContent(eventJson, Encoding.UTF8, "application/json"); - return await HttpClient.PostAsync("/services/collector/event", content); - } - - internal void DisposeHttpClient() - { - if (HttpClient != null) + public void DisableSSLValidation() { - HttpClient.Dispose(); - HttpClient = null; + DisposeHttpClient(); + _httpClientHandler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + CreateHttpClient(); } - if (SplunkClientHandler != null) + public async Task SendAsync(SplunkPayload payload, bool disableSplunkSSL = false) { - SplunkClientHandler.Dispose(); - SplunkClientHandler = null; + try + { + if (payload == null) + return null; + + if (!HasHecToken || !HasHecUrl) + return null; + + if (disableSplunkSSL) + DisableSSLValidation(); + + _httpClient ??= new HttpClient(_httpClientHandler ?? new HttpClientHandler()) + { + BaseAddress = new Uri(SplunkHecUrl) + }; + + var eventObject = new + { + @event = payload.EventData, + sourcetype = payload.SourceType, + host = payload.Host + }; + + var json = JsonHelper.ToJson(eventObject); + + var request = new HttpRequestMessage(HttpMethod.Post, "services/collector/event") + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + + return await _httpClient.SendAsync(request); + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + return null; + } + } + + internal void DisposeHttpClient() + { + try + { + _httpClient?.Dispose(); + _httpClientHandler?.Dispose(); + } + catch + { + // Silent fail + } + finally + { + _httpClient = null; + _httpClientHandler = null; + } + } + + public void Dispose() + { + DisposeHttpClient(); + GC.SuppressFinalize(this); + } + + ~Splunk() + { + Dispose(); } } - - public void Dispose() - { - DisposeHttpClient(); - GC.SuppressFinalize(this); - } - - ~Splunk() - { - Dispose(); - } -} \ No newline at end of file +} diff --git a/EonaCat.Logger/Servers/Syslog/Syslog.cs b/EonaCat.Logger/Servers/Syslog/Syslog.cs index e896071..f29c5e7 100644 --- a/EonaCat.Logger/Servers/Syslog/Syslog.cs +++ b/EonaCat.Logger/Servers/Syslog/Syslog.cs @@ -1,194 +1,191 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; - -namespace EonaCat.Logger.Servers.Syslog; -// 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. - -/// -/// Syslog server. -/// -public class Syslog : IDisposable -{ - const int MaxUdpPacketSize = 4096; - private string _Hostname = "127.0.0.1"; - private int _Port = 514; - public bool IsConnected { get; private set; } - - internal UdpClient Udp; - - public Syslog() { } - - /// - /// Syslog server - /// - /// - /// - /// - /// - /// - /// - public Syslog(string hostname = "127.0.0.1", int port = 514, string nickName = null, List typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false) - { - Hostname = hostname; - Port = port; - Nickname = nickName ?? IpPort; - TypesToLog = typesToLog; - ConvertToRfc5424 = convertToRfc5424; - ConvertToRfc3164 = convertToRfc3164; - } - - public string Hostname - { - get => _Hostname; - set - { - if (string.IsNullOrEmpty(value)) +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EonaCat.Logger.Servers.Syslog +{ + // This file is part of the EonaCat project(s) released under the Apache License. + public class Syslog : IDisposable + { + private const int MaxUdpPacketSize = 4096; + private string _hostname = "127.0.0.1"; + private int _port = 514; + private UdpClient _udp; + public event EventHandler OnException; + + public bool IsConnected { get; private set; } + public string Nickname { get; set; } + public string IpPort => $"{_hostname}:{_port}"; + public bool SupportsTcp { get; set; } + public List TypesToLog { get; set; } + public bool ConvertToRfc5424 { get; set; } + public bool ConvertToRfc3164 { get; set; } + + public Syslog() { } + + public Syslog(string hostname = "127.0.0.1", int port = 514, string nickName = null, List typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false) + { + Hostname = hostname; + Port = port; + Nickname = nickName ?? IpPort; + TypesToLog = typesToLog; + ConvertToRfc5424 = convertToRfc5424; + ConvertToRfc3164 = convertToRfc3164; + } + + public string Hostname + { + get => _hostname; + set { - throw new ArgumentNullException(nameof(Hostname)); + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(Hostname)); + + _hostname = value; + SetUdp(); + } + } + + public int Port + { + get => _port; + set + { + if (value < 0) + throw new ArgumentException("Port must be zero or greater."); + + _port = value; + SetUdp(); + } + } + + internal void SetUdp() + { + try + { + DisposeUdp(); + _udp = new UdpClient(_hostname, _port); + IsConnected = true; + } + catch + { + IsConnected = false; + } + } + + internal void DisposeUdp() + { + IsConnected = false; + + try + { + _udp?.Close(); + _udp?.Dispose(); + } + catch + { + // Swallow cleanup exceptions } - _Hostname = value; - SetUdp(); - } - } - - public int Port - { - get => _Port; - set - { - if (value < 0) + _udp = null; + } + + public async Task WriteAsync(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return; + + var data = Encoding.UTF8.GetBytes(message); + await SendAsync(data); + } + + public async Task WriteAsync(byte[] data) + { + if (data is { Length: > 0 }) + await SendAsync(data); + } + + private async Task SendAsync(byte[] data) + { + if (_udp == null) + return; + + if (data.Length <= MaxUdpPacketSize) { - throw new ArgumentException("Port must be zero or greater."); + try + { + await _udp.SendAsync(data, data.Length); + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + IsConnected = false; + } + } + else if (SupportsTcp) + { + await SendViaTcpAsync(data); + } + else + { + await SendUdpInChunksAsync(data, MaxUdpPacketSize); } - - _Port = value; - SetUdp(); - } - } - - public string IpPort => _Hostname + ":" + _Port; - public bool SupportsTcp { get; set; } - public string Nickname { get; set; } - public List TypesToLog { get; set; } - public bool ConvertToRfc5424 { get; set; } - public bool ConvertToRfc3164 { get; set; } - - internal void SetUdp() - { - try - { - DisposeUdp(); - Udp = new UdpClient(_Hostname, _Port); - IsConnected = true; - } - catch - { - IsConnected = false; - } - } - - internal void DisposeUdp() - { - if (Udp != null) - { - IsConnected = false; - - try - { - Udp?.Close(); - Udp?.Dispose(); - } - catch - { - // Do nothing - } - Udp = null; - } - } - - public void Dispose() - { - DisposeUdp(); - GC.SuppressFinalize(this); - } - - public async Task WriteAsync(string message) - { - var data = Encoding.UTF8.GetBytes(message); - await SendAsync(data); - } - - public async Task WriteAsync(byte[] data) - { - await SendAsync(data); - } - - private async Task SendAsync(byte[] data) - { - if (data == null) - { - return; } - if (Udp == null) + private async Task SendViaTcpAsync(byte[] data) { - return; + try + { + using var tcpClient = new TcpClient(); + await tcpClient.ConnectAsync(_hostname, _port); + + var stream = tcpClient.GetStream(); + await stream.WriteAsync(data, 0, data.Length); + await stream.FlushAsync(); + } + catch + { + // TCP failure fallback is silent here + } } - if (data.Length <= MaxUdpPacketSize) + private async Task SendUdpInChunksAsync(byte[] data, int chunkSize) { - // Send via UDP (single packet) - await Udp?.SendAsync(data, data.Length); + int offset = 0; + byte[] buffer = ArrayPool.Shared.Rent(chunkSize); + + try + { + while (offset < data.Length) + { + int size = Math.Min(chunkSize, data.Length - offset); + Buffer.BlockCopy(data, offset, buffer, 0, size); + await _udp.SendAsync(buffer, size); + offset += size; + } + } + catch + { + IsConnected = false; + } + finally + { + ArrayPool.Shared.Return(buffer); + } } - else if (SupportsTcp) + + public void Dispose() { - // Send via TCP if supported - await SendViaTcpAsync(this, data); + DisposeUdp(); + GC.SuppressFinalize(this); } - else + + ~Syslog() { - // Chunk large messages for UDP - await SendUdpInChunksAsync(this, data, MaxUdpPacketSize); + Dispose(); } } - - /// - /// Sends a message via TCP to a syslog server. - /// - private static async Task SendViaTcpAsync(Servers.Syslog.Syslog server, byte[] data) - { - if (server == null) - { - return; - } - - using var tcpClient = new System.Net.Sockets.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(Servers.Syslog.Syslog 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); - } - } - - ~Syslog() - { - Dispose(); - } -} \ No newline at end of file +} diff --git a/EonaCat.Logger/Servers/Tcp/Tcp.cs b/EonaCat.Logger/Servers/Tcp/Tcp.cs index 8c5f59d..6a1f851 100644 --- a/EonaCat.Logger/Servers/Tcp/Tcp.cs +++ b/EonaCat.Logger/Servers/Tcp/Tcp.cs @@ -2,35 +2,30 @@ using System.Collections.Generic; using System.Net.Sockets; using System.Text; +using System.Threading; using System.Threading.Tasks; + namespace EonaCat.Logger.Servers.Tcp { // 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. public class Tcp : IDisposable { - private string _Hostname = "127.0.0.1"; - private int _Port = 514; - internal TcpClient _tcp; + private string _hostname = "127.0.0.1"; + private int _port = 514; + private TcpClient _tcp; + private readonly SemaphoreSlim _sendLock = new(1, 1); + public event EventHandler OnException; public bool IsConnected { get; private set; } public string Nickname { get; set; } - public string IpPort => _Hostname + ":" + _Port; + public string IpPort => $"{_hostname}:{_port}"; + public List TypesToLog { get; set; } - /// - /// Tcp Server - /// - /// - /// - /// - /// - /// - /// public Tcp(string hostname, int port, string nickname = null, List typesToLog = null) { - _Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname)); - _Port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater."); - + _hostname = hostname ?? throw new ArgumentNullException(nameof(hostname)); + _port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater."); Nickname = nickname ?? IpPort; TypesToLog = typesToLog; @@ -39,42 +34,36 @@ namespace EonaCat.Logger.Servers.Tcp public string Hostname { - get => _Hostname; + get => _hostname; set { - if (string.IsNullOrEmpty(value)) - { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(Hostname)); - } - _Hostname = value; + _hostname = value; SetTcp(); } } public int Port { - get => _Port; + get => _port; set { if (value < 0) - { throw new ArgumentException("Port must be zero or greater."); - } - _Port = value; + _port = value; SetTcp(); } } - public List TypesToLog { get; set; } - internal void SetTcp() { try { DisposeTcp(); - _tcp = new TcpClient(_Hostname, _Port); + _tcp = new TcpClient(_hostname, _port); IsConnected = true; } catch @@ -85,80 +74,67 @@ namespace EonaCat.Logger.Servers.Tcp internal void DisposeTcp() { - if (_tcp != null) - { - IsConnected = false; + IsConnected = false; - try - { - _tcp.Close(); - _tcp.Dispose(); - _tcp = null; - } - catch - { - // Do nothing - } + try + { + _tcp?.Close(); + _tcp?.Dispose(); + } + catch + { + // Silent fail + } + + _tcp = null; + } + + public async Task WriteAsync(byte[] data) + { + if (data == null || data.Length == 0) return; + await InternalWriteAsync(data); + } + + public async Task WriteAsync(string data) + { + if (string.IsNullOrWhiteSpace(data)) return; + var bytes = Encoding.UTF8.GetBytes(data); + await InternalWriteAsync(bytes); + } + + private async Task InternalWriteAsync(byte[] data) + { + await _sendLock.WaitAsync(); + try + { + if (!IsConnected || _tcp == null) + SetTcp(); + + if (!IsConnected || _tcp == null) + return; + + var stream = _tcp.GetStream(); + await stream.WriteAsync(data, 0, data.Length); + await stream.FlushAsync(); + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + IsConnected = false; + } + finally + { + _sendLock.Release(); } } public void Dispose() { DisposeTcp(); + _sendLock.Dispose(); GC.SuppressFinalize(this); } - public async Task WriteAsync(byte[] data) - { - if (_tcp == null) - { - return; - } - - if (data == null) - { - return; - } - - if (!IsConnected) - { - SetTcp(); - } - - if (IsConnected) - { - using var stream = _tcp.GetStream(); - await stream.WriteAsync(data, 0, data.Length); - await stream.FlushAsync(); - } - } - - public async Task WriteAsync(string data) - { - if (_tcp == null) - { - return; - } - - if (string.IsNullOrEmpty(data)) - { - return; - } - - if (!IsConnected) - { - SetTcp(); - } - - if (IsConnected) - { - var sendData = Encoding.UTF8.GetBytes(data); - using var stream = _tcp.GetStream(); - await stream.WriteAsync(sendData, 0, sendData.Length); - await stream.FlushAsync(); - } - } - ~Tcp() { Dispose(); diff --git a/EonaCat.Logger/Servers/Udp/Udp.cs b/EonaCat.Logger/Servers/Udp/Udp.cs index a5a0ca2..9f175cc 100644 --- a/EonaCat.Logger/Servers/Udp/Udp.cs +++ b/EonaCat.Logger/Servers/Udp/Udp.cs @@ -1,36 +1,33 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Net.Sockets; using System.Text; +using System.Threading; using System.Threading.Tasks; + namespace EonaCat.Logger.Servers.Udp { // 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. public class Udp : IDisposable { - const int MaxUdpPacketSize = 4096; - private string _Hostname = "127.0.0.1"; - private int _Port = 514; - internal UdpClient _udp; + private const int MaxUdpPacketSize = 4096; + private string _hostname = "127.0.0.1"; + private int _port = 514; + private UdpClient _udp; + private readonly SemaphoreSlim _sendLock = new(1, 1); + public event EventHandler OnException; public bool IsConnected { get; private set; } public string Nickname { get; set; } - public string IpPort => _Hostname + ":" + _Port; + public string IpPort => $"{_hostname}:{_port}"; + public List TypesToLog { get; set; } - /// - /// Udp Server - /// - /// - /// - /// - /// - /// - /// public Udp(string hostname, int port, string nickname = null, List typesToLog = null) { - _Hostname = hostname ?? throw new ArgumentNullException(nameof(hostname)); - _Port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater."); + _hostname = hostname ?? throw new ArgumentNullException(nameof(hostname)); + _port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater."); TypesToLog = typesToLog; Nickname = nickname ?? IpPort; SetUdp(); @@ -38,42 +35,39 @@ namespace EonaCat.Logger.Servers.Udp public string Hostname { - get => _Hostname; + get => _hostname; set { - if (string.IsNullOrEmpty(value)) - { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(Hostname)); - } - _Hostname = value; + _hostname = value; SetUdp(); } } public int Port { - get => _Port; + get => _port; set { if (value < 0) - { throw new ArgumentException("Port must be zero or greater."); - } - _Port = value; + _port = value; SetUdp(); } } - public List TypesToLog { get; set; } - internal void SetUdp() { try { DisposeUdp(); - _udp = new UdpClient(_Hostname, _Port); + _udp = new UdpClient(_hostname, _port) + { + DontFragment = false + }; IsConnected = true; } catch @@ -84,92 +78,88 @@ namespace EonaCat.Logger.Servers.Udp internal void DisposeUdp() { - if (_udp != null) + IsConnected = false; + + try + { + _udp?.Close(); + _udp?.Dispose(); + } + catch + { + // Silently ignore + } + + _udp = null; + } + + public async Task WriteAsync(byte[] data, bool dontFragment = false) + { + if (data == null || data.Length == 0) + return; + + await _sendLock.WaitAsync(); + try + { + if (!IsConnected) + SetUdp(); + + if (!IsConnected || _udp == null) + return; + + _udp.DontFragment = dontFragment; + + await SendChunksAsync(data); + } + finally + { + _sendLock.Release(); + } + } + + public async Task WriteAsync(string data, bool dontFragment = false) + { + if (string.IsNullOrWhiteSpace(data)) + return; + + var bytes = Encoding.UTF8.GetBytes(data); + await WriteAsync(bytes, dontFragment); + } + + private async Task SendChunksAsync(byte[] data) + { + int offset = 0; + int length = data.Length; + byte[] buffer = ArrayPool.Shared.Rent(MaxUdpPacketSize); + + try + { + while (offset < length) + { + int chunkSize = Math.Min(MaxUdpPacketSize, length - offset); + Buffer.BlockCopy(data, offset, buffer, 0, chunkSize); + await _udp.SendAsync(buffer, chunkSize); + offset += chunkSize; + } + } + catch (Exception ex) { IsConnected = false; - - try - { - _udp?.Close(); - _udp.Dispose(); - } - catch - { - // Do nothing - } - _udp = null; + OnException?.Invoke(this, ex); + } + finally + { + ArrayPool.Shared.Return(buffer); } } public void Dispose() { DisposeUdp(); + _sendLock.Dispose(); GC.SuppressFinalize(this); } - public async Task WriteAsync(byte[] data, bool dontFragment = false) - { - if (_udp == null || data == null) - { - return; - } - - if (!IsConnected) - { - SetUdp(); - } - - if (IsConnected) - { - _udp.DontFragment = dontFragment; - - int maxChunkSize = MaxUdpPacketSize; - int offset = 0; - - while (offset < data.Length) - { - int chunkSize = Math.Min(maxChunkSize, data.Length - offset); - byte[] chunk = new byte[chunkSize]; - Array.Copy(data, offset, chunk, 0, chunkSize); - - await _udp?.SendAsync(chunk, chunk.Length); - offset += chunkSize; - } - } - } - - public async Task WriteAsync(string data, bool dontFragment = false) - { - if (_udp == null || string.IsNullOrEmpty(data)) - { - return; - } - - if (!IsConnected) - { - SetUdp(); - } - - if (IsConnected) - { - var sendData = Encoding.UTF8.GetBytes(data); - _udp.DontFragment = dontFragment; - - int maxChunkSize = MaxUdpPacketSize; - int offset = 0; - - while (offset < sendData.Length) - { - int chunkSize = Math.Min(maxChunkSize, sendData.Length - offset); - byte[] chunk = new byte[chunkSize]; - Array.Copy(sendData, offset, chunk, 0, chunkSize); - - await _udp?.SendAsync(chunk, chunk.Length); - offset += chunkSize; - } - } - } - ~Udp() { Dispose(); diff --git a/EonaCat.Logger/Servers/Zabbix/API/ZabbixApi.cs b/EonaCat.Logger/Servers/Zabbix/API/ZabbixApi.cs index 297c811..429114e 100644 --- a/EonaCat.Logger/Servers/Zabbix/API/ZabbixApi.cs +++ b/EonaCat.Logger/Servers/Zabbix/API/ZabbixApi.cs @@ -5,144 +5,107 @@ using System.IO; using System.Net; using System.Text; -namespace EonaCat.Logger.Servers.Zabbix.API; -// 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. - -public class ZabbixApi +namespace EonaCat.Logger.Servers.Zabbix.API { /// - /// Zabbix Api + /// Zabbix API client for authentication and communication with Zabbix server. /// - /// - /// - /// - /// - public ZabbixApi(string user, string password, string zabbixURL, bool basicAuth) + public class ZabbixApi { - _user = user; - _password = password; - _zabbixURL = zabbixURL; + private readonly string _user; + private readonly string _password; + private readonly string _zabbixUrl; + private readonly string _basicAuth; + private string _auth; + public event EventHandler OnException; - if (basicAuth) + public bool IsLoggedIn { get; private set; } + + public ZabbixApi(string user, string password, string zabbixUrl, bool useBasicAuth = false) { - _basicAuth = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(_user + ":" + _password)); + _user = user ?? throw new ArgumentNullException(nameof(user)); + _password = password ?? throw new ArgumentNullException(nameof(password)); + _zabbixUrl = zabbixUrl ?? throw new ArgumentNullException(nameof(zabbixUrl)); + + _basicAuth = useBasicAuth + ? Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{_user}:{_password}")) + : null; } - _auth = null; - } - - public ZabbixApi(string user, string password, string zabbixURL) : this(user, password, zabbixURL, false) - { - - } - - private readonly string _user; - private readonly string _password; - private readonly string _zabbixURL; - private string _auth; - private readonly string _basicAuth = null; - - /// - /// Determines if the user is logged in - /// - public bool IsLoggedIn { get; set; } - - /// - /// Logs in the user - /// - public void Login() - { - dynamic authentication = new ExpandoObject(); - authentication.user = _user; - authentication.password = _password; - - ZabbixApiResponse zabbixApiResponse = ResponseAsObject("user.login", authentication); - - _auth = zabbixApiResponse.Result; - IsLoggedIn = !string.IsNullOrEmpty(_auth); - } - - /// - /// Logs out the user - /// - /// - public bool Logout() - { - ZabbixApiResponse zbxResponse = ResponseAsObject("user.logout", new string[] { }); - var result = zbxResponse.Result; - return result; - } - - /// - /// Generic method to send a request to the Zabbix API - /// - /// - /// - /// - public string ResponseAsJson(string method, object parameters) - { - ZabbixApiRequest zbxRequest = new ZabbixApiRequest("2.0", method, 1, _auth, parameters); - string jsonParameters = JsonHelper.ToJson(zbxRequest); - return SendRequest(jsonParameters); - } - - /// - /// Generic method to send a request to the Zabbix API - /// - /// - /// - /// - public ZabbixApiResponse ResponseAsObject(string method, object parameters) - { - ZabbixApiRequest zbxRequest = new ZabbixApiRequest("2.0", method, 1, _auth, parameters); - string jsonParameters = JsonHelper.ToJson(zbxRequest); - return CreateResponse(SendRequest(jsonParameters)); - } - - /// - /// Creates a ZabbixApiResponse object from a JSON string - /// - /// - /// - private ZabbixApiResponse CreateResponse(string json) - { - ZabbixApiResponse zbxResponse = JsonHelper.ToObject(json); - return zbxResponse; - } - - /// - /// Sends a request to the Zabbix API - /// - /// - /// - private string SendRequest(string jsonParams) - { - WebRequest request = WebRequest.Create(_zabbixURL); - - if (_basicAuth != null) + /// + /// Login and retrieve authentication token from Zabbix API. + /// + public void Login() { - request.Headers.Add("Authorization", "Basic " + _basicAuth); + dynamic authParams = new ExpandoObject(); + authParams.user = _user; + authParams.password = _password; + + var response = ResponseAsObject("user.login", authParams); + _auth = response?.Result; + IsLoggedIn = !string.IsNullOrEmpty(_auth); } - request.ContentType = "application/json-rpc"; - request.Method = "POST"; - string jsonResult; - - using (var streamWriter = new StreamWriter(request.GetRequestStream())) + /// + /// Logout from Zabbix API. + /// + public bool Logout() { - streamWriter.Write(jsonParams); - streamWriter.Flush(); - streamWriter.Close(); + var response = ResponseAsObject("user.logout", Array.Empty()); + return response?.Result ?? false; } - WebResponse response = request.GetResponse(); - using (var streamReader = new StreamReader(response.GetResponseStream())) + /// + /// Send a Zabbix API request and return JSON response as string. + /// + public string ResponseAsJson(string method, object parameters) { - jsonResult = streamReader.ReadToEnd(); + var request = CreateRequest(method, parameters); + var jsonParams = JsonHelper.ToJson(request); + return SendRequest(jsonParams); } - return jsonResult; + /// + /// Send a Zabbix API request and return deserialized response. + /// + public ZabbixApiResponse ResponseAsObject(string method, object parameters) + { + var request = CreateRequest(method, parameters); + var jsonParams = JsonHelper.ToJson(request); + var responseJson = SendRequest(jsonParams); + return JsonHelper.ToObject(responseJson); + } + + private ZabbixApiRequest CreateRequest(string method, object parameters) => + new("2.0", method, 1, _auth, parameters); + + private string SendRequest(string jsonParams) + { + try + { + var request = WebRequest.Create(_zabbixUrl); + request.Method = "POST"; + request.ContentType = "application/json-rpc"; + + if (!string.IsNullOrEmpty(_basicAuth)) + { + request.Headers.Add("Authorization", $"Basic {_basicAuth}"); + } + + using (var writer = new StreamWriter(request.GetRequestStream())) + { + writer.Write(jsonParams); + } + + using var response = request.GetResponse(); + using var reader = new StreamReader(response.GetResponseStream()); + return reader.ReadToEnd(); + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + return null; + } + } } - } diff --git a/Testers/EonaCat.Logger.Test.Web/Program.cs b/Testers/EonaCat.Logger.Test.Web/Program.cs index cbb5f8a..86179cf 100644 --- a/Testers/EonaCat.Logger.Test.Web/Program.cs +++ b/Testers/EonaCat.Logger.Test.Web/Program.cs @@ -7,10 +7,6 @@ using EonaCat.Logger.Test.Web; using EonaCat.Versioning.Helpers; using EonaCat.Web.RateLimiter; using EonaCat.Web.RateLimiter.Endpoints.Extensions; -using EonaCat.Web.Tracer.Extensions; -using EonaCat.Web.Tracer.Models; -using Microsoft.Extensions.Logging; -using System.Runtime.Versioning; var builder = WebApplication.CreateBuilder(args); int onLogCounter = 0;