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); + } + } +}