? labels = null,
+ int batchSize = 50,
+ int batchIntervalMs = 1000,
+ string? bearerToken = null,
+ string? basicUser = null,
+ string? basicPassword = null,
+ HttpClient? httpClient = null,
+ LogLevel minimumLevel = LogLevel.Trace)
+ {
+ _flows.Add(new LokiFlow(
+ lokiUrl, labels, batchSize, batchIntervalMs,
+ bearerToken, basicUser, basicPassword, httpClient, minimumLevel));
+ return this;
+ }
+
+ ///
+ /// Adds the which captures caller member/file/line.
+ ///
+ public LogBuilder BoostWithCallerInfo()
+ {
+ _boosters.Add(new CallerInfoBooster());
+ return this;
+ }
+
+
+ /// Adds a stable event fingerprint for grouping failures.
+ public LogBuilder BoostWithExceptionFingerprint()
+ {
+ _boosters.Add(new ExceptionFingerprintBooster());
+ return this;
+ }
+
+ /// Adds runtime health information to every event.
+ public LogBuilder BoostWithHealthSnapshot()
+ {
+ _boosters.Add(new HealthSnapshotBooster());
+ return this;
+ }
+
+ /// Adds a schema version field to every log event.
+ public LogBuilder BoostWithSchemaVersion(string version = "1.0")
+ {
+ _boosters.Add(new SchemaVersionBooster(version));
+ return this;
+ }
+
+ /// Adds a monotonic sequence number to every event.
+ public LogBuilder BoostWithSequence()
+ {
+ _boosters.Add(new SequenceBooster());
+ return this;
+ }
+
+ ///
+ /// Creates a dependency-free compatibility facade exposing adapters for
+ /// Serilog, log4net and NLog style integrations.
+ ///
+ public Compatibility.LoggingCompatibilityFacade AsCompatibility()
+ {
+ return new Compatibility.LoggingCompatibilityFacade(Build());
+ }
+
+ ///
+ /// Creates a Serilog compatible adapter without adding Serilog dependency.
+ ///
+ public Compatibility.EonaCatSerilogAdapter AsSerilog()
+ {
+ return new Compatibility.EonaCatSerilogAdapter(Build());
+ }
+
+ ///
+ /// Creates a log4net compatible adapter without adding log4net dependency.
+ ///
+ public Compatibility.EonaCatLog4NetAdapter AsLog4Net()
+ {
+ return new Compatibility.EonaCatLog4NetAdapter(Build());
+ }
+
+ ///
+ /// Creates an NLog compatible adapter without adding NLog dependency.
+ ///
+ public Compatibility.EonaCatNLogAdapter AsNLog()
+ {
+ return new Compatibility.EonaCatNLogAdapter(Build());
+ }
+
+}
diff --git a/EonaCat.LogStack/Telemetry/TelemetryClient.cs b/EonaCat.LogStack/Telemetry/TelemetryClient.cs
new file mode 100644
index 0000000..efa476f
--- /dev/null
+++ b/EonaCat.LogStack/Telemetry/TelemetryClient.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+namespace EonaCat.LogStack.Telemetry;
+public sealed class TelemetryClient : IDisposable
+{
+ private readonly HttpClient _http = new HttpClient();
+ private readonly Uri _endpoint;
+ public TelemetryClient(string endpoint) => _endpoint = new Uri(endpoint.TrimEnd('/') + "/telemetry");
+ public Task TrackAsync(TelemetryEvent evt, CancellationToken token = default)
+ {
+ var json = JsonSerializer.Serialize(evt);
+ return _http.PostAsync(_endpoint, new StringContent(json, Encoding.UTF8, "application/json"), token);
+ }
+ public void Dispose() => _http.Dispose();
+}
diff --git a/EonaCat.LogStack/Telemetry/TelemetryDashboard.cs b/EonaCat.LogStack/Telemetry/TelemetryDashboard.cs
new file mode 100644
index 0000000..4e54b4a
--- /dev/null
+++ b/EonaCat.LogStack/Telemetry/TelemetryDashboard.cs
@@ -0,0 +1,11 @@
+namespace EonaCat.LogStack.Telemetry;
+public static class TelemetryDashboard
+{
+ public static string Html => @"
+EonaCat Telemetry
+EonaCat LogStack Telemetry
+No external dependencies. Connect to /telemetry to ingest.
+";
+}
diff --git a/EonaCat.LogStack/Telemetry/TelemetryEvent.cs b/EonaCat.LogStack/Telemetry/TelemetryEvent.cs
new file mode 100644
index 0000000..b27bdf0
--- /dev/null
+++ b/EonaCat.LogStack/Telemetry/TelemetryEvent.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+namespace EonaCat.LogStack.Telemetry;
+public sealed class TelemetryEvent
+{
+ public string Name { get; set; } = "";
+ public string Level { get; set; } = "Information";
+ public long TimestampUnixMs { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ public double DurationMs { get; set; }
+ public long Value { get; set; }
+ public string? TraceId { get; set; }
+ public Dictionary? Tags { get; set; }
+}
diff --git a/EonaCat.LogStack/Telemetry/TelemetryServer.cs b/EonaCat.LogStack/Telemetry/TelemetryServer.cs
new file mode 100644
index 0000000..2d171ba
--- /dev/null
+++ b/EonaCat.LogStack/Telemetry/TelemetryServer.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Net;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+namespace EonaCat.LogStack.Telemetry;
+public sealed class TelemetryServer : IDisposable
+{
+ private readonly HttpListener _listener = new();
+ public ConcurrentQueue Events { get; } = new();
+ public int Count => Events.Count;
+ public TelemetryServer(string prefix = "http://localhost:5155/")
+ {
+ _listener.Prefixes.Add(prefix);
+ }
+ public async Task StartAsync(CancellationToken token = default)
+ {
+ _listener.Start();
+ while (!token.IsCancellationRequested)
+ {
+ var ctx = await _listener.GetContextAsync();
+ _ = Task.Run(async () =>
+ {
+ if (ctx.Request.HttpMethod == "POST" && ctx.Request.Url?.AbsolutePath == "/telemetry")
+ {
+ using var r = new StreamReader(ctx.Request.InputStream);
+ var item = JsonSerializer.Deserialize(await r.ReadToEndAsync());
+ if (item != null)
+ {
+ Events.Enqueue(item);
+ }
+ }
+ else if (ctx.Request.Url?.AbsolutePath == "/")
+ {
+ var html = TelemetryDashboard.Html;
+ var bytes = System.Text.Encoding.UTF8.GetBytes(html);
+ ctx.Response.ContentType = "text/html";
+ await ctx.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
+ }
+ ctx.Response.Close();
+ });
+ }
+ }
+ public void Dispose() => _listener.Close();
+}