Skip to content

ADR 0002

Benno Kress edited this page Oct 22, 2026 · 6 revisions

ADR 0002: Die App legt ihre Log-Level selbst fest

Status: angenommen
Herkunft: #22 (Befund und Entscheidung), #32 (Filter statt logPriorityRange), #69 (keine Methoden für Fehler und Exceptions), #84 (Enum der App hinter einem Protokoll, Priorität als Int-Rohwert), #73 (@LogLevels und @Emoji, Filter speichern ihre Stufen), #72 (Umsetzung)

Kontext

Bis Version 2 war LogLevel ein Enum mit fest eingebauten Stufen. Die drei unteren Stufen hatten je zwei Namen: .debug und .xcode, .verbose und .staging, .info und .production. Die Paare hatten dasselbe Emoji und dieselbe Priorität, waren aber verschiedene Fälle und damit ungleich. PrettyLogProxy.logX, logS und logP erzeugten trotzdem die klassischen Fälle. Die semantischen kamen nur an, wenn jemand sie von Hand übergab. Ein Target, der über die Stufe verzweigte, musste deshalb beide Namen behandeln. Die Apps des Betreuers benutzen beide Benennungen: die eigenen Apps eher die klassische, die Apps mit Staging-Umgebung eher die semantische.

Wenn die App ihre Stufen selbst festlegt, muss das Paket zwei Dinge trotzdem sicherstellen: Keine zwei Stufen einer App dürfen dieselbe Priorität haben, sonst ist offen, welche ein Filter meint. Und Apps mit einem OSLog- oder Backend-Target verzweigen per switch über die Stufe. Eine neu hinzugefügte Stufe soll dort nicht still in einem default landen.

Entscheidung

  • Das Paket liefert keine Log-Level. Die App deklariert ihre Stufen als eigenes Enum mit dem Makro @LogLevels, eine Zeile pro Stufe:

    @LogLevels
    nonisolated enum AppLogLevel: Int {
    
        @Emoji("🟤") case debug = 100
        @Emoji("🔴") case error = 900
        @Emoji("🟢") case info = 500
    
    }
  • Der Int-Rohwert ist die Priorität. Stufen sind über die Priorität geordnet, nie über die Reihenfolge der Fälle, damit die Fälle alphabetisch sortiert werden können.

  • LogLevel ist ein Protokoll: RawRepresentable mit Int, CaseIterable, Comparable, Hashable und Sendable, mit den Anforderungen emoji und _$logLevelsMacro. priority und < liefert eine Protokoll-Erweiterung, priority ist keine Anforderung und kann deshalb nicht ersetzt werden.

  • @LogLevels erzeugt emoji, allCases, die Markierung _$logLevelsMacro und die Konformität. Es lehnt alles ab, was kein Enum mit Int-Rohwert ist, und jeden Fall ohne @Emoji oder mit mehr als einer Stufe. @Emoji erzeugt selbst nichts. Die erzeugten Mitglieder übernehmen public oder package vom Enum. Das Makro ist der einzige vorgesehene Weg: Eine von Hand geschriebene Konformität scheitert an der Markierung, deren Typ LogLevelsMacroMarker nur das Makro sinnvoll befüllt.

  • LogTarget hat die primäre assoziierte Stufe Level. Die App führt ihre Targets als [any LogTarget<AppLogLevel>], PrettyLogProxy.log ist generisch über die Stufe, und Swift leitet sie aus as: und to: ab.

  • Targets filtern über LogLevelPriorityFilter<Level>: .all, .none, .only(_:...), .from(_:), .until(_:) und .between(_:and:). Jeder Filter speichert die Stufen, die er annimmt, ausgewählt aus allCases. .only(.info) nimmt genau .info an, und zwei Filter sind gleich, wenn sie dieselben Stufen annehmen. In Debug-Builds prüft der Filter, dass keine zwei Stufen dieselbe Priorität haben.

  • PrettyLogProxy hat keine Methoden, die an eine bestimmte Stufe gebunden sind. Die Stufe ist bei log(_:sensitiveMessages:joinedBy:as:category:to:) ein Pflichtparameter. Eigene Methoden für Fehler und Exceptions hat das Paket nicht, die baut die App mit sensitiveMessages selbst.

  • README und Demo-App zeigen als Vorschlag die Werte aus Version 2, mit den klassischen Namen als Hauptbeispiel und den semantischen als Alternative. Die höchste Stufe bekommt 1000, weil ein Rohwert ein Literal sein muss und Int.max keines ist.

Begründung

Welche Stufen eine App braucht und wie sie heißen, hängt von der App ab. Eine App ohne Staging-Umgebung kann mit "Staging" nichts anfangen, eine mit Staging-Umgebung will genau das sagen. Ein Paket, das eine Benennung vorgibt, zwingt einen Teil der Apps zum Umbenennen. Ein Paket, das beide anbietet, hat zwei Namen für dieselbe Stufe. Wenn die App die Stufen selbst festlegt, gibt es nur eine Schreibweise pro App. Das passt zu ADR 0001: Targets und Log-Funktionen legt die App ebenfalls selbst an.

Das Enum erfüllt beide Anforderungen aus dem Kontext. Swift lehnt zwei Fälle mit demselben Rohwert ab, also kann eine App zwei Stufen keine gleiche Priorität geben, und ein switch über die Stufen ist ohne default vollständig: Eine neue Stufe ohne Zuordnung kompiliert nicht. Ohne Makro bräuchte jede Stufe zwei Stellen, den Fall mit Rohwert und einen Zweig im switch für das Emoji. Das Makro bringt beides in eine Zeile und lässt die Prüfung der Prioritäten trotzdem beim Compiler.

Konsequenzen

  • Breaking Change für jede App, die das Paket nutzt. Sie braucht ein Enum mit ihren Stufen, ihre Wrapper rufen PrettyLogProxy.log(..., as:) statt logD und Co., und ihre Targets nennen den Typ ihrer Stufen in emit und levelFilter.
  • Die Mindestversionen steigen auf iOS 16, Mac Catalyst 16, macOS 13 und tvOS 16, weil any LogTarget<Level> Laufzeitunterstützung braucht, die es erst ab diesen Versionen gibt. watchOS bleibt bei 9.
  • Das Paket hängt von swift-syntax ab, in Version 604. Eine App, deren andere Pakete eine andere Version verlangen, kann das Paket nicht auflösen. Xcode fragt beim ersten Bauen, ob die App dem Makro vertraut.
  • Ein Target, das nur Emoji und Priorität braucht, kann generisch bleiben und in Apps mit verschiedenen Stufen laufen. Ein Target, das Stufen auf etwas anderes abbildet, etwa auf OSLogType, ist an das Enum einer App gebunden.
  • Eine Bibliothek mit eigenem Stufen-Enum kann die Targets einer App mit anderem Enum nicht übernehmen, weil any LogTarget<A> und any LogTarget<B> verschiedene Typen sind.
  • Ein Typ, der die erzeugte Markierung von Hand abschreibt, kompiliert trotzdem. Das lässt sich nicht verhindern. Gleiche Prioritäten in seinem allCases meldet nur die Prüfung im Filter, und nur in Debug-Builds.
  • Eine Methode für Key Events mit eigener Signatur gibt es im Paket nicht. Ob eine App bei Key Events sensible Teile zulässt, entscheidet ihr eigener Wrapper.

Verworfene Alternativen

  • Die semantischen Namen als Aliase der klassischen. static var xcode: LogLevel { .debug } hätte für die meisten Aufrufer ohne Änderung kompiliert. Verworfen, weil ein Target, der beide Namen getrennt behandelt, danach still ein anderes Verhalten bekommen hätte. Außerdem gäbe es weiter zwei Namen für dieselbe Stufe.
  • Nur die semantischen Stufen behalten. Hätte die doppelte Schreibweise mit einem Compilerfehler statt still entfernt. Verworfen, weil damit allen Apps die semantische Benennung vorgegeben wäre, auch denen ohne Staging-Umgebung.
  • logX, logS und logP erzeugen ihre eigenen Fälle. Verworfen, weil die doppelte Schreibweise bliebe und Targets weiter beide Namen behandeln müssten.
  • LogLevel als Struktur aus Emoji und Priorität, die Stufen als Erweiterung der App. Eine Zeile pro Stufe ohne Makro und ohne Abhängigkeit. Verworfen, weil das Paket die Stufen einer App nie gesammelt sieht und deshalb zwei Stufen mit gleicher Priorität nicht verhindern kann, und weil ein switch über Werte einen default braucht.
  • Ordnung über die Reihenfolge der Fälle. Ein Enum ohne Rohwert, Comparable vom Compiler erzeugt. Verworfen, weil Fälle üblicherweise alphabetisch sortiert werden und das Sortieren die Bedeutung still geändert hätte.
  • AnyLogTarget<Level> als Typ-Löschung. Hätte iOS 15 gehalten. Verworfen, weil jede App jedes Target darin hätte einpacken müssen.
  • Text als Rohwert (case debug = "🟤 100", ohne Makro). Braucht keine Abhängigkeit, kompiliert aber mit zwei Stufen gleicher Priorität, solange das Emoji verschieden ist, und mit Tippfehlern wie 6OO.
  • Makro ohne Rohwert (@Level("🟤", priority: 100) case debug). Liest sich etwas besser, verlegt aber die Prüfung eindeutiger Prioritäten vom Compiler in eigenen Makro-Code.

PrettyLog

Hier stehen Architekturentscheidungen im heute gültigen Stand. Vorgänge bleiben Issues.

Clone this wiki locally