From 347ddfea017f4f31c5c00e6889e8e454f8aec090 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 10 Oct 2026 20:30:23 +0200 Subject: [PATCH] =?UTF-8?q?Das=20Protokoll=20kannte=20nur,=20was=20jemand?= =?UTF-8?q?=20ausdr=C3=BCcklich=20hineinschrieb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start, Ende, ein paar Fehler — mehr stand nie drin. Dabei meldet der Server längst, was er tut: „Scan gestartet", „Strom angelegt", „Untertitel aus … gezogen", „Bibliothek ohne Treffer". Das ging alles an `ILogger` und damit in die Container-Ausgabe, wo es zwischen den EF-Abfragen untergeht. Eine Senke hängt das Tagebuch jetzt an die Protokollkette. Nur Flaros eigene Kategorien — `Microsoft.*` käme mit einer Zeile je Datenbank- abfrage und machte das Protokoll nach einem Scan unlesbar. Debug und Trace bleiben draussen, die sind fürs Entwickeln. Damit fallen die beiden Stellen weg, an denen ich gestern von Hand ins Tagebuch geschrieben habe; sie kommen jetzt über denselben Weg wie alles andere. Der Name der Quelle steht davor: „Streamkoordinator: …". Nebenbei: das Tagebuch wird von Hand gebaut statt aus dem Behälter geholt. Die Senke braucht es, bevor der Behälter steht, und ein `BuildServiceProvider()` dafür legte eine zweite Ausgabe an — zwei Tagebücher mit zwei Schlössern, die sich beim Schreiben ins Gehege kommen. Co-Authored-By: Claude Opus 5 --- server/src/Flaro.Api/Program.cs | 16 ++++++- server/src/Flaro.Api/Streamkoordinator.cs | 16 ++----- server/src/Flaro.Api/Tagebuch.cs | 58 +++++++++++++++++++++++ 3 files changed, 75 insertions(+), 15 deletions(-) diff --git a/server/src/Flaro.Api/Program.cs b/server/src/Flaro.Api/Program.cs index 97c5b61..c35a758 100644 --- a/server/src/Flaro.Api/Program.cs +++ b/server/src/Flaro.Api/Program.cs @@ -190,7 +190,20 @@ builder.Services.AddHostedService(sp => sp.GetRequiredService()); // Im Arbeitsspeicher und nur dort: wer gerade was schaut, ist eine Frage // ueber den Augenblick. Nach einem Neustart lautet die Antwort: niemand. builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +// **Von Hand gebaut, nicht aus dem Behälter geholt.** Die Protokollsenke +// braucht ihn, bevor der Behälter steht; ein `BuildServiceProvider()` dafür +// legte eine zweite Ausgabe an — zwei Tagebücher mit zwei Schlössern, die +// sich beim Schreiben ins Gehege kommen. +var tagebuch = new Tagebuch(pfade, TimeProvider.System); +builder.Services.AddSingleton(tagebuch); + +// **Was der Server ohnehin meldet, gehört auch ins Tagesprotokoll.** +// +// „Scan gestartet", „Strom angelegt", „Untertitel gezogen" standen längst +// im Quelltext — sie gingen nur an die Container-Ausgabe, wo sie zwischen +// den EF-Abfragen untergehen. Die Senke nimmt nur Flaros eigene Kategorien; +// `Microsoft.*` bliebe sonst mit einer Zeile je Datenbankabfrage darin. +builder.Logging.AddProvider(new Tagebuchsenke(tagebuch)); // **Die Schlüssel der Datenverschlüsselung gehören ins Volume.** // @@ -383,7 +396,6 @@ if (Directory.Exists(oberfläche)) // macht den Unterschied zwischen „dauert eben" und „stürzt ab". Steht mitten // im Scan ein Start, weiss man es beim nächsten Blick ins Protokoll. { - var tagebuch = app.Services.GetRequiredService(); var leben = app.Services.GetRequiredService(); leben.ApplicationStarted.Register(() => diff --git a/server/src/Flaro.Api/Streamkoordinator.cs b/server/src/Flaro.Api/Streamkoordinator.cs index ad017ae..27b336e 100644 --- a/server/src/Flaro.Api/Streamkoordinator.cs +++ b/server/src/Flaro.Api/Streamkoordinator.cs @@ -63,7 +63,6 @@ public sealed class Streamkoordinator( FfmpegOptions ffmpeg, Schluesselbilder schluesselbilder, TimeProvider clock, - Tagebuch tagebuch, ILogger log) : IDisposable { private readonly ConcurrentDictionary _sitzungen = new(); @@ -198,8 +197,7 @@ public sealed class Streamkoordinator( + (plan.Hoehe is { } h ? $", auf {h}p" : "") + $" — Schlüsselbildsuche {suche} ms, Vorbereitung {uhr.ElapsedMilliseconds} ms."; - log.LogInformation("Strom {Kennung}: {Meldung}", kennung, meldung); - tagebuch.Schreiben(meldung); + log.LogInformation("{Meldung}", meldung); return sitzung; } @@ -406,17 +404,9 @@ public sealed class Streamkoordinator( } sitzung.Fehler = text.Trim(); - log.LogWarning("Strom {Kennung}: ffmpeg endete mit {Code}: {Fehler}", - sitzung.Kennung, prozess.ExitCode, sitzung.Fehler); + log.LogError("Wiedergabe gescheitert: ffmpeg endete mit {Code}. {Fehler}", + prozess.ExitCode, sitzung.Fehler); - // **Auch ins Tagesprotokoll, nicht nur in die Konsole.** Die - // Fehlermeldung im Spieler verweist aufs Protokoll von heute — - // und dort stand nichts, weil `ILogger` in den Container-Ausgaben - // landet und das Tagebuch eine eigene Datei ist. Der Hinweis war - // damit eine Sackgasse. - tagebuch.Schreiben( - $"Wiedergabe gescheitert: ffmpeg endete mit {prozess.ExitCode}. {sitzung.Fehler}", - Tagebuch.Art.Fehler); }); _ = Task.Run(() => prozess.StandardOutput.ReadToEndAsync()); diff --git a/server/src/Flaro.Api/Tagebuch.cs b/server/src/Flaro.Api/Tagebuch.cs index 18800e2..b3c6c79 100644 --- a/server/src/Flaro.Api/Tagebuch.cs +++ b/server/src/Flaro.Api/Tagebuch.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text; +using Microsoft.Extensions.Logging.Abstractions; namespace Flaro.Api; @@ -175,3 +176,60 @@ public sealed class Tagebuch } } } + +/// Leitet, was der Server ohnehin meldet, ins Tagesprotokoll. +/// +/// +/// +/// Statt an jeder Stelle zweimal zu schreiben. Das Protokoll kannte +/// nur, was jemand ausdrücklich hineinschrieb: Start, Ende, ein paar Fehler. +/// „Scan gestartet", „Strom angelegt", „Untertitel gezogen" stehen längst im +/// Quelltext — sie gingen nur an ILogger und damit in die +/// Container-Ausgabe, wo sie zwischen EF-Abfragen untergehen. +/// +/// +/// Nur die eigenen Kategorien. Microsoft.* und +/// System.* bleiben draussen — dort kommt je Datenbankabfrage eine +/// Zeile, und das Protokoll wäre nach einem Scan unlesbar. +/// +/// +public sealed class Tagebuchsenke(Tagebuch tagebuch) : ILoggerProvider +{ + public ILogger CreateLogger(string kategorie) => + kategorie.StartsWith("Flaro", StringComparison.Ordinal) + ? new Schreiber(tagebuch, Kurzname(kategorie)) + : NullLogger.Instance; + + public void Dispose() { } + + /// „Flaro.Api.Streamkoordinator" wird zu „Streamkoordinator". + private static string Kurzname(string kategorie) => + kategorie[(kategorie.LastIndexOf('.') + 1)..]; + + private sealed class Schreiber(Tagebuch tagebuch, string woher) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + // Debug und Trace bleiben draussen: die sind fürs Entwickeln da. + public bool IsEnabled(LogLevel stufe) => stufe >= LogLevel.Information; + + public void Log( + LogLevel stufe, EventId kennung, TState zustand, Exception? fehler, + Func text) + { + if (!IsEnabled(stufe)) return; + + var art = stufe switch + { + >= LogLevel.Error => Tagebuch.Art.Fehler, + LogLevel.Warning => Tagebuch.Art.Warnung, + _ => Tagebuch.Art.Info, + }; + + var satz = text(zustand, fehler); + if (fehler is not null) satz += $" — {fehler.Message}"; + + tagebuch.Schreiben($"{woher}: {satz}", art); + } + } +}