using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace PulseRadar { public class PulseRadarClient : IDisposable { private const string DefaultEndpoint = "https://ingest.pulseradar.cloud/v1/logs"; private const int FlushSize = 50; private static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(5); private readonly string _apiKey; private readonly string _source; private readonly string? _serverId; private readonly string _endpoint; private readonly HttpClient _http; private readonly ConcurrentQueue> _queue = new(); private readonly Timer _timer; private readonly CancellationTokenSource _cts = new(); public PulseRadarClient(string apiKey, string source = "dotnet", string? serverId = null, string? endpoint = null) { _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); _source = source; _serverId = serverId; _endpoint = endpoint ?? DefaultEndpoint; _http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; _http.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}"); _http.DefaultRequestHeaders.Add("User-Agent", "pulseradar-dotnet/1.0"); if (_serverId != null) _http.DefaultRequestHeaders.Add("X-Server-Id", _serverId); _timer = new Timer(_ => _ = FlushAsync(), null, FlushInterval, FlushInterval); } public void Debug(string message, Dictionary? fields = null) => Log("debug", message, fields); public void Info(string message, Dictionary? fields = null) => Log("info", message, fields); public void Warning(string message, Dictionary? fields = null) => Log("warning", message, fields); public void Error(string message, Dictionary? fields = null) => Log("error", message, fields); public void Critical(string message, Dictionary? fields = null) => Log("critical", message, fields); public void CaptureException(Exception ex, Dictionary? extra = null) { if (ex == null) return; var fields = extra ?? new Dictionary(); fields["exception_type"] = ex.GetType().FullName ?? ex.GetType().Name; var frames = ex.StackTrace?.Split('\n'); if (frames != null) { var stack = string.Join(" | ", frames.Length > 5 ? frames[..5] : frames); fields["stack"] = stack.Trim(); } Log("error", $"{ex.GetType().Name}: {ex.Message}", fields); } public async Task FlushAsync() { var batch = new List>(); while (_queue.TryDequeue(out var entry) && batch.Count < 500) batch.Add(entry); if (batch.Count == 0) return; try { var json = JsonSerializer.Serialize(batch); var content = new StringContent(json, Encoding.UTF8, "application/json"); await _http.PostAsync(_endpoint, content, _cts.Token); } catch { /* silently ignore send errors */ } } private void Log(string level, string message, Dictionary? fields) { var entry = new Dictionary { ["level"] = level, ["message"] = message, ["source"] = _source, ["timestamp"] = DateTime.UtcNow.ToString("o"), ["fields"] = fields ?? new Dictionary() }; _queue.Enqueue(entry); if (_queue.Count >= FlushSize) _ = FlushAsync(); } public void Dispose() { _timer.Dispose(); FlushAsync().GetAwaiter().GetResult(); _cts.Cancel(); _http.Dispose(); } } }