diff --git a/Inspectron.Epson.Tests/Telemetry/LoopbackInsinTelemetryTests.cs b/Inspectron.Epson.Tests/Telemetry/LoopbackInsinTelemetryTests.cs new file mode 100644 index 0000000..d6d2ddb --- /dev/null +++ b/Inspectron.Epson.Tests/Telemetry/LoopbackInsinTelemetryTests.cs @@ -0,0 +1,74 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Inspectron.Epson.PrintServer.Telemetry; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Inspectron.Epson.Tests.Telemetry; + +public class LoopbackInsinTelemetryTests +{ + private const int TestPort = 47823; + + [Fact] + public async Task Emit_posts_json_payload_to_loopback() + { + using var listener = new HttpListener(); + listener.Prefixes.Add($"http://127.0.0.1:{TestPort}/"); + listener.Start(); + + using var http = new HttpClient(); + var telemetry = new LoopbackInsinTelemetry(http, NullLogger.Instance); + telemetry.Emit("service.started", "version=1.0.14"); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var ctxTask = listener.GetContextAsync(); + var completed = await Task.WhenAny(ctxTask, Task.Delay(Timeout.Infinite, cts.Token)); + Assert.Same(ctxTask, completed); + var ctx = ctxTask.Result; + + Assert.Equal("POST", ctx.Request.HttpMethod); + Assert.Equal("/events", ctx.Request.Url!.AbsolutePath); + using var sr = new StreamReader(ctx.Request.InputStream, Encoding.UTF8); + var body = await sr.ReadToEndAsync(); + using var doc = JsonDocument.Parse(body); + Assert.Equal("service.started", doc.RootElement.GetProperty("kind").GetString()); + Assert.Equal("version=1.0.14", doc.RootElement.GetProperty("message").GetString()); + var at = doc.RootElement.GetProperty("at").GetString(); + Assert.False(string.IsNullOrEmpty(at)); + Assert.EndsWith("Z", at); + + ctx.Response.StatusCode = 200; + ctx.Response.Close(); + listener.Stop(); + } + + [Fact] + public async Task Emit_swallows_connection_refused_and_does_not_throw() + { + using var http = new HttpClient(); + var telemetry = new LoopbackInsinTelemetry(http, NullLogger.Instance); + var ex = Record.Exception(() => telemetry.Emit("service.started", "version=1.0.14")); + Assert.Null(ex); + await Task.Delay(200); + } + + [Fact] + public async Task Emit_swallows_500_response_and_does_not_throw() + { + using var listener = new HttpListener(); + listener.Prefixes.Add($"http://127.0.0.1:{TestPort}/"); + listener.Start(); + + using var http = new HttpClient(); + var telemetry = new LoopbackInsinTelemetry(http, NullLogger.Instance); + telemetry.Emit("service.started", "version=1.0.14"); + + var ctx = await listener.GetContextAsync(); + ctx.Response.StatusCode = 500; + ctx.Response.Close(); + + await Task.Delay(200); + listener.Stop(); + } +} diff --git a/Inspectron.Epson/PrintServer/Telemetry/LoopbackInsinTelemetry.cs b/Inspectron.Epson/PrintServer/Telemetry/LoopbackInsinTelemetry.cs new file mode 100644 index 0000000..d780462 --- /dev/null +++ b/Inspectron.Epson/PrintServer/Telemetry/LoopbackInsinTelemetry.cs @@ -0,0 +1,56 @@ +using System.Net.Http.Json; +using Microsoft.Extensions.Logging; + +namespace Inspectron.Epson.PrintServer.Telemetry; + +public sealed class LoopbackInsinTelemetry : IInsinTelemetry +{ + private const string Endpoint = "http://127.0.0.1:47823/events"; + private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(2); + private static readonly long WarnIntervalTicks = TimeSpan.FromMinutes(5).Ticks / TimeSpan.TicksPerMillisecond; + + private readonly HttpClient _http; + private readonly ILogger _logger; + private long _lastWarnMs = -WarnIntervalTicks; + + public LoopbackInsinTelemetry(HttpClient http, ILogger logger) + { + _http = http; + _logger = logger; + } + + public void Emit(string kind, string message) + { + var payload = new + { + kind, + message, + at = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + }; + _ = Task.Run(() => SendAsync(payload)); + } + + private async Task SendAsync(object payload) + { + try + { + using var cts = new CancellationTokenSource(RequestTimeout); + using var resp = await _http.PostAsJsonAsync(Endpoint, payload, cts.Token); + if (!resp.IsSuccessStatusCode) + MaybeWarn($"insin loopback returned {(int)resp.StatusCode}"); + } + catch (Exception ex) + { + MaybeWarn($"insin loopback unreachable at 127.0.0.1:47823 ({ex.GetType().Name}), dropping events"); + } + } + + private void MaybeWarn(string message) + { + var now = Environment.TickCount64; + var last = Interlocked.Read(ref _lastWarnMs); + if (now - last < WarnIntervalTicks) return; + if (Interlocked.CompareExchange(ref _lastWarnMs, now, last) != last) return; + _logger.LogWarning("{Message}", message); + } +}