9.1 Serilog wiring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 16:08:07 +02:00
parent d6ebf89468
commit fddb674a79
3 changed files with 47 additions and 6 deletions

View File

@@ -210,7 +210,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor.
## Phase 9 — Observability ## Phase 9 — Observability
### [ ] 9.1 Serilog wiring ### [x] 9.1 Serilog wiring
- `UseSerilog` with console + rolling file sinks (`logs/frame-processor-.log`, daily). - `UseSerilog` with console + rolling file sinks (`logs/frame-processor-.log`, daily).
- Structured fields: `FrameName`, `MacAddress`, `InputSource` (file/url), `InputBytes`, `OutputBytes`, `ElapsedMs`, `MqttPublished`. - Structured fields: `FrameName`, `MacAddress`, `InputSource` (file/url), `InputBytes`, `OutputBytes`, `ElapsedMs`, `MqttPublished`.
- Log one info line per upload at completion. - Log one info line per upload at completion.

View File

@@ -1,3 +1,4 @@
using System.Diagnostics;
using FrameProcessor.Concurrency; using FrameProcessor.Concurrency;
using FrameProcessor.Configuration; using FrameProcessor.Configuration;
using FrameProcessor.Domain; using FrameProcessor.Domain;
@@ -19,6 +20,7 @@ public sealed class FramesController : ControllerBase
private readonly MqttPublisher _mqtt; private readonly MqttPublisher _mqtt;
private readonly IImageUrlFetcher _urlFetcher; private readonly IImageUrlFetcher _urlFetcher;
private readonly FrameLockProvider _locks; private readonly FrameLockProvider _locks;
private readonly ILogger<FramesController> _logger;
public FramesController( public FramesController(
FramesRegistry frames, FramesRegistry frames,
@@ -26,7 +28,8 @@ public sealed class FramesController : ControllerBase
ImageStore store, ImageStore store,
MqttPublisher mqtt, MqttPublisher mqtt,
IImageUrlFetcher urlFetcher, IImageUrlFetcher urlFetcher,
FrameLockProvider locks) FrameLockProvider locks,
ILogger<FramesController> logger)
{ {
_frames = frames; _frames = frames;
_pipeline = pipeline; _pipeline = pipeline;
@@ -34,11 +37,14 @@ public sealed class FramesController : ControllerBase
_mqtt = mqtt; _mqtt = mqtt;
_urlFetcher = urlFetcher; _urlFetcher = urlFetcher;
_locks = locks; _locks = locks;
_logger = logger;
} }
[HttpPost("{name}/image")] [HttpPost("{name}/image")]
public async Task<IActionResult> UploadImage(string name, CancellationToken cancellationToken) public async Task<IActionResult> UploadImage(string name, CancellationToken cancellationToken)
{ {
var stopwatch = Stopwatch.StartNew();
if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame)) if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame))
{ {
return NotFound(); return NotFound();
@@ -58,7 +64,8 @@ public sealed class FramesController : ControllerBase
pngBytes = _pipeline.Process(stream, frame); 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, [FromBody] ImageUrlRequest? body,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var stopwatch = Stopwatch.StartNew();
if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame)) if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame))
{ {
return NotFound(); return NotFound();
@@ -93,20 +102,42 @@ public sealed class FramesController : ControllerBase
} }
byte[] pngBytes; byte[] pngBytes;
long inputBytes;
await using (source) await using (source)
{ {
inputBytes = source.CanSeek ? source.Length : 0;
pngBytes = _pipeline.Process(source, frame); 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<IActionResult> FinishUploadAsync(Frame frame, byte[] pngBytes, CancellationToken cancellationToken) private async Task<IActionResult> FinishUploadAsync(
Frame frame,
byte[] pngBytes,
string inputSource,
long inputBytes,
Stopwatch stopwatch,
CancellationToken cancellationToken)
{ {
await _store.WriteAsync(frame.Mac, pngBytes, cancellationToken).ConfigureAwait(false); await _store.WriteAsync(frame.Mac, pngBytes, cancellationToken).ConfigureAwait(false);
var publishResult = await _mqtt.PublishAsync(frame.Mac, 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 return Ok(new
{ {
@@ -114,7 +145,7 @@ public sealed class FramesController : ControllerBase
mac = frame.Mac.ToString(), mac = frame.Mac.ToString(),
url = $"/i/{frame.Mac}.png", url = $"/i/{frame.Mac}.png",
processedAt = DateTimeOffset.UtcNow, processedAt = DateTimeOffset.UtcNow,
mqttPublished = publishResult == PublishResult.Success, mqttPublished,
}); });
} }

View File

@@ -6,11 +6,21 @@ using FrameProcessor.Mqtt;
using FrameProcessor.Storage; using FrameProcessor.Storage;
using FrameProcessor.UrlFetch; using FrameProcessor.UrlFetch;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Serilog;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("frames.json", optional: false, reloadOnChange: true); 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.AddControllers();
builder.Services.AddOptions<MqttOptions>() builder.Services.AddOptions<MqttOptions>()