git_bsmd/bsmd.util/LokiAppender.cs
2025-05-26 09:01:59 +02:00

156 lines
4.6 KiB
C#

// Copyright (c) 2025 schick Informatik
// Description: Appender for Loki/Grafana remote logging
//
namespace bsmd.util
{
using log4net.Appender;
using log4net.Core;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
public class LokiAppender : AppenderSkeleton
{
#region Fields
private static readonly HttpClient _httpClient = new HttpClient();
private readonly ConcurrentQueue<Tuple<string, string>> _logQueue = new ConcurrentQueue<Tuple<string, string>>();
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private bool _batchingStarted = false;
private readonly object _batchLock = new object();
#endregion
#region Properties
public string LokiUrl { get; set; }
public string ApplicationName { get; set; } = "log4net-app";
public int BatchSize { get; set; } = 10;
public int BatchIntervalMs { get; set; } = 2000;
public int MaxRetries { get; set; } = 5;
#endregion
#region overrides
protected override void Append(LoggingEvent loggingEvent)
{
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + "000000";
string message = RenderLoggingEvent(loggingEvent);
_logQueue.Enqueue(new Tuple<string, string>(timestamp, message));
// Start background batch loop once
if (!_batchingStarted)
{
lock (_batchLock)
{
if (!_batchingStarted)
{
_ = Task.Run(() => BatchLoop(_cts.Token));
_batchingStarted = true;
}
}
}
}
private async Task BatchLoop(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await Task.Delay(BatchIntervalMs, ct);
var batch = new List<Tuple<string, string>>();
while (batch.Count < BatchSize && _logQueue.TryDequeue(out var entry))
batch.Add(entry);
if (batch.Count == 0) continue;
var payload = new
{
streams = new[]
{
new
{
stream = new
{
app = ApplicationName,
level = "info" // Optional: capture log level from batch
},
values = batch.ConvertAll(e => new[] { e.Item1, e.Item2 })
}
}
};
string json = JsonConvert.SerializeObject(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
await SendWithRetry(content, MaxRetries, ct);
}
catch (OperationCanceledException)
{
// Graceful shutdown
}
catch (Exception ex)
{
ErrorHandler.Error("Error in Loki batch loop", ex);
}
}
}
private async Task SendWithRetry(HttpContent content, int maxRetries, CancellationToken ct)
{
int attempt = 0;
TimeSpan delay = TimeSpan.FromSeconds(1);
while (attempt <= maxRetries && !ct.IsCancellationRequested)
{
try
{
var response = await _httpClient.PostAsync(LokiUrl, content, ct);
if (response.IsSuccessStatusCode)
return;
attempt++;
await Task.Delay(delay, ct);
delay = TimeSpan.FromSeconds(delay.TotalSeconds * 2); // exponential backoff
}
catch
{
attempt++;
await Task.Delay(delay, ct);
delay = TimeSpan.FromSeconds(delay.TotalSeconds * 2);
}
}
ErrorHandler.Error($"Failed to send logs to Loki after {attempt} retries.");
}
protected override void OnClose()
{
_cts.Cancel();
base.OnClose();
}
#endregion
}
}