9.1 Serilog wiring
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
Reference in New Issue
Block a user