Das Protokoll kannte nur, was jemand ausdrücklich hineinschrieb

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-10 20:30:23 +02:00
co-authored by Claude Opus 5
parent 19ec17da19
commit 347ddfea01
3 changed files with 75 additions and 15 deletions
+14 -2
View File
@@ -190,7 +190,20 @@ builder.Services.AddHostedService(sp => sp.GetRequiredService<Wartungsplan>());
// Im Arbeitsspeicher und nur dort: wer gerade was schaut, ist eine Frage // Im Arbeitsspeicher und nur dort: wer gerade was schaut, ist eine Frage
// ueber den Augenblick. Nach einem Neustart lautet die Antwort: niemand. // ueber den Augenblick. Nach einem Neustart lautet die Antwort: niemand.
builder.Services.AddSingleton<PlaybackSessions>(); builder.Services.AddSingleton<PlaybackSessions>();
builder.Services.AddSingleton<Tagebuch>(); // **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.** // **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 // 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. // im Scan ein Start, weiss man es beim nächsten Blick ins Protokoll.
{ {
var tagebuch = app.Services.GetRequiredService<Tagebuch>();
var leben = app.Services.GetRequiredService<IHostApplicationLifetime>(); var leben = app.Services.GetRequiredService<IHostApplicationLifetime>();
leben.ApplicationStarted.Register(() => leben.ApplicationStarted.Register(() =>
+3 -13
View File
@@ -63,7 +63,6 @@ public sealed class Streamkoordinator(
FfmpegOptions ffmpeg, FfmpegOptions ffmpeg,
Schluesselbilder schluesselbilder, Schluesselbilder schluesselbilder,
TimeProvider clock, TimeProvider clock,
Tagebuch tagebuch,
ILogger<Streamkoordinator> log) : IDisposable ILogger<Streamkoordinator> log) : IDisposable
{ {
private readonly ConcurrentDictionary<Guid, Eintrag> _sitzungen = new(); private readonly ConcurrentDictionary<Guid, Eintrag> _sitzungen = new();
@@ -198,8 +197,7 @@ public sealed class Streamkoordinator(
+ (plan.Hoehe is { } h ? $", auf {h}p" : "") + (plan.Hoehe is { } h ? $", auf {h}p" : "")
+ $" — Schlüsselbildsuche {suche} ms, Vorbereitung {uhr.ElapsedMilliseconds} ms."; + $" — Schlüsselbildsuche {suche} ms, Vorbereitung {uhr.ElapsedMilliseconds} ms.";
log.LogInformation("Strom {Kennung}: {Meldung}", kennung, meldung); log.LogInformation("{Meldung}", meldung);
tagebuch.Schreiben(meldung);
return sitzung; return sitzung;
} }
@@ -406,17 +404,9 @@ public sealed class Streamkoordinator(
} }
sitzung.Fehler = text.Trim(); sitzung.Fehler = text.Trim();
log.LogWarning("Strom {Kennung}: ffmpeg endete mit {Code}: {Fehler}", log.LogError("Wiedergabe gescheitert: ffmpeg endete mit {Code}. {Fehler}",
sitzung.Kennung, prozess.ExitCode, sitzung.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()); _ = Task.Run(() => prozess.StandardOutput.ReadToEndAsync());
+58
View File
@@ -1,5 +1,6 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
namespace Flaro.Api; namespace Flaro.Api;
@@ -175,3 +176,60 @@ public sealed class Tagebuch
} }
} }
} }
/// <summary>Leitet, was der Server ohnehin meldet, ins Tagesprotokoll.</summary>
///
/// <remarks>
/// <para>
/// <b>Statt an jeder Stelle zweimal zu schreiben.</b> 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 <c>ILogger</c> und damit in die
/// Container-Ausgabe, wo sie zwischen EF-Abfragen untergehen.
/// </para>
/// <para>
/// <b>Nur die eigenen Kategorien.</b> <c>Microsoft.*</c> und
/// <c>System.*</c> bleiben draussen — dort kommt je Datenbankabfrage eine
/// Zeile, und das Protokoll wäre nach einem Scan unlesbar.
/// </para>
/// </remarks>
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() { }
/// <summary>„Flaro.Api.Streamkoordinator" wird zu „Streamkoordinator".</summary>
private static string Kurzname(string kategorie) =>
kategorie[(kategorie.LastIndexOf('.') + 1)..];
private sealed class Schreiber(Tagebuch tagebuch, string woher) : ILogger
{
public IDisposable? BeginScope<TState>(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<TState>(
LogLevel stufe, EventId kennung, TState zustand, Exception? fehler,
Func<TState, Exception?, string> 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);
}
}
}