diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 04534f6..ddd093f 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -210,7 +210,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor. ## Phase 9 — Observability -### [ ] 9.1 Serilog wiring +### [x] 9.1 Serilog wiring - `UseSerilog` with console + rolling file sinks (`logs/frame-processor-.log`, daily). - Structured fields: `FrameName`, `MacAddress`, `InputSource` (file/url), `InputBytes`, `OutputBytes`, `ElapsedMs`, `MqttPublished`. - Log one info line per upload at completion. diff --git a/src/FrameProcessor/Controllers/FramesController.cs b/src/FrameProcessor/Controllers/FramesController.cs index bfb2fd2..7fca916 100644 --- a/src/FrameProcessor/Controllers/FramesController.cs +++ b/src/FrameProcessor/Controllers/FramesController.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using FrameProcessor.Concurrency; using FrameProcessor.Configuration; using FrameProcessor.Domain; @@ -19,6 +20,7 @@ public sealed class FramesController : ControllerBase private readonly MqttPublisher _mqtt; private readonly IImageUrlFetcher _urlFetcher; private readonly FrameLockProvider _locks; + private readonly ILogger _logger; public FramesController( FramesRegistry frames, @@ -26,7 +28,8 @@ public sealed class FramesController : ControllerBase ImageStore store, MqttPublisher mqtt, IImageUrlFetcher urlFetcher, - FrameLockProvider locks) + FrameLockProvider locks, + ILogger logger) { _frames = frames; _pipeline = pipeline; @@ -34,11 +37,14 @@ public sealed class FramesController : ControllerBase _mqtt = mqtt; _urlFetcher = urlFetcher; _locks = locks; + _logger = logger; } [HttpPost("{name}/image")] public async Task UploadImage(string name, CancellationToken cancellationToken) { + var stopwatch = Stopwatch.StartNew(); + if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame)) { return NotFound(); @@ -58,7 +64,8 @@ public sealed class FramesController : ControllerBase pngBytes = _pipeline.Process(stream, frame); } - return await FinishUploadAsync(frame, pngBytes, cancellationToken).ConfigureAwait(false); + return await FinishUploadAsync(frame, pngBytes, "file", file.Length, stopwatch, cancellationToken) + .ConfigureAwait(false); } } @@ -68,6 +75,8 @@ public sealed class FramesController : ControllerBase [FromBody] ImageUrlRequest? body, CancellationToken cancellationToken) { + var stopwatch = Stopwatch.StartNew(); + if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame)) { return NotFound(); @@ -93,20 +102,42 @@ public sealed class FramesController : ControllerBase } byte[] pngBytes; + long inputBytes; await using (source) { + inputBytes = source.CanSeek ? source.Length : 0; pngBytes = _pipeline.Process(source, frame); } - return await FinishUploadAsync(frame, pngBytes, cancellationToken).ConfigureAwait(false); + return await FinishUploadAsync(frame, pngBytes, "url", inputBytes, stopwatch, cancellationToken) + .ConfigureAwait(false); } } - private async Task FinishUploadAsync(Frame frame, byte[] pngBytes, CancellationToken cancellationToken) + private async Task FinishUploadAsync( + Frame frame, + byte[] pngBytes, + string inputSource, + long inputBytes, + Stopwatch stopwatch, + CancellationToken cancellationToken) { await _store.WriteAsync(frame.Mac, pngBytes, cancellationToken).ConfigureAwait(false); var publishResult = await _mqtt.PublishAsync(frame.Mac, cancellationToken).ConfigureAwait(false); + var mqttPublished = publishResult == PublishResult.Success; + + stopwatch.Stop(); + + _logger.LogInformation( + "Processed upload for {FrameName} ({MacAddress}) source={InputSource} in={InputBytes} out={OutputBytes} elapsed={ElapsedMs}ms mqtt={MqttPublished}", + frame.Name.Value, + frame.Mac.ToString(), + inputSource, + inputBytes, + pngBytes.LongLength, + stopwatch.ElapsedMilliseconds, + mqttPublished); return Ok(new { @@ -114,7 +145,7 @@ public sealed class FramesController : ControllerBase mac = frame.Mac.ToString(), url = $"/i/{frame.Mac}.png", processedAt = DateTimeOffset.UtcNow, - mqttPublished = publishResult == PublishResult.Success, + mqttPublished, }); } diff --git a/src/FrameProcessor/Program.cs b/src/FrameProcessor/Program.cs index e6a81f5..d4fffbb 100644 --- a/src/FrameProcessor/Program.cs +++ b/src/FrameProcessor/Program.cs @@ -6,11 +6,21 @@ using FrameProcessor.Mqtt; using FrameProcessor.Storage; using FrameProcessor.UrlFetch; using Microsoft.Extensions.Options; +using Serilog; var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("frames.json", optional: false, reloadOnChange: true); +builder.Host.UseSerilog((context, services, configuration) => configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File( + path: "logs/frame-processor-.log", + rollingInterval: RollingInterval.Day)); + builder.Services.AddControllers(); builder.Services.AddOptions()