feat(core): ManaAppLog + appGroup/logSubsystem in ManaAppConfig (v1.7.0)
Audit 2026-05-17 V4. Ersetzt das hand-getippte Log.swift-Boilerplate
in jeder App durch einen Config-getriebenen Wrapper.
Neu:
- `ManaAppLog` — Factory fuer OSLog-Logger gegen ein ManaAppConfig.
Standard-Kategorien app/auth/api/db/web, plus `category("…")` fuer
app-spezifische Kategorien.
- `ManaAppConfig.appGroup: String?` (default nil) — Single-Source fuer
den App-Group-String, der heute in jeder App 3-4× hardcoded steht.
- `ManaAppConfig.logSubsystem: String` (default = keychainService) —
Subsystem fuer ManaAppLog.
Nichts breaking — beide neuen Felder haben Default-Implementations,
DefaultManaAppConfig.init hat zwei zusaetzliche optionale Parameter.
Tests: 4 neue ManaAppConfig-Tests + 5 neue ManaAppLog-Tests.
85/85 gruen (vorher 76/76).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
20c30fc321
commit
4ce22ac74e
5 changed files with 242 additions and 1 deletions
50
CHANGELOG.md
50
CHANGELOG.md
|
|
@ -4,6 +4,56 @@ Alle Änderungen werden hier dokumentiert. Format orientiert an
|
|||
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
|
||||
[Semver](https://semver.org).
|
||||
|
||||
## [1.7.0] — 2026-05-17
|
||||
|
||||
Minor — **`ManaAppLog`** + `ManaAppConfig.appGroup`/`logSubsystem`.
|
||||
Audit 2026-05-17 V4. Ersetzt das hand-getippte `Log.swift`-Boilerplate
|
||||
in jeder App durch einen Config-getriebenen Wrapper.
|
||||
|
||||
### Neu
|
||||
|
||||
- `ManaAppLog` (public struct, Sendable) — Factory für OSLog-Logger
|
||||
gegen ein `ManaAppConfig`. Standard-Kategorien `app`/`auth`/`api`/
|
||||
`db`/`web`, plus `category("…")` für app-spezifische Kategorien.
|
||||
- `ManaAppConfig.appGroup: String?` (default `nil`) — Single-Source
|
||||
für den App-Group-String, der heute in jeder App 3-4× hardcoded
|
||||
steht. Apps ohne Widget/ShareExt setzen weiterhin nichts.
|
||||
- `ManaAppConfig.logSubsystem: String` (default = `keychainService`)
|
||||
— Subsystem für `ManaAppLog`. In allen Apps heute schon
|
||||
`ev.mana.<app>`, deshalb default sinnvoll.
|
||||
|
||||
### Geändert
|
||||
|
||||
- Nichts breaking. Beide neuen Felder haben Default-Implementations
|
||||
im Protocol-Extension, bestehende Konsumenten von `ManaAppConfig`
|
||||
brauchen nichts anzupassen.
|
||||
- `DefaultManaAppConfig.init` hat zwei zusätzliche optionale Parameter
|
||||
(`appGroup`, `logSubsystem`), beide mit `nil`-Defaults — Quellkompatibel.
|
||||
|
||||
### Tests
|
||||
|
||||
- 4 neue ManaAppConfig-Tests + 5 neue ManaAppLog-Tests. 85/85 grün
|
||||
(vorher 76/76).
|
||||
|
||||
### Migrations-Hinweis
|
||||
|
||||
Apps können ihre lokale `Log.swift` von ~13 LOC auf ~5 LOC schrumpfen:
|
||||
|
||||
```swift
|
||||
import ManaCore
|
||||
|
||||
enum Log {
|
||||
private static let mana = ManaAppLog(AppConfig.manaAppConfig)
|
||||
static let app = mana.app
|
||||
static let auth = mana.auth
|
||||
static let api = mana.api
|
||||
static let study = mana.category("study") // app-spezifisch
|
||||
}
|
||||
```
|
||||
|
||||
Plus `AppConfig.manaAppConfig` kann `appGroup: "group.ev.mana.<app>"`
|
||||
ergänzen, damit der App-Group-String single-source ist.
|
||||
|
||||
## [1.6.0] — 2026-05-17
|
||||
|
||||
Minor — **`ManaTheme`-Variants** in ManaTokens. Acht Web-Themes aus
|
||||
|
|
|
|||
|
|
@ -21,6 +21,31 @@ public protocol ManaAppConfig: Sendable {
|
|||
/// Apple-Developer-Team-ID provisioniert sein und das Entitlement
|
||||
/// `keychain-access-groups` mit demselben Wert tragen.
|
||||
var keychainAccessGroup: String? { get }
|
||||
|
||||
/// App-Group für Daten-Sharing zwischen App ↔ Widget ↔ ShareExt.
|
||||
/// Üblich `group.ev.mana.<app>`. `nil` für Apps ohne Extensions.
|
||||
///
|
||||
/// Single-Source für den App-Group-String, der heute in jeder App
|
||||
/// 3-4× hardcoded steht (AppConfig + App-Entitlement + Widget-
|
||||
/// Entitlement + ShareExt-Entitlement). Die Entitlements bleiben
|
||||
/// hardcoded (das verlangt iOS), aber im Swift-Code ist der Wert
|
||||
/// damit single-source.
|
||||
var appGroup: String? { get }
|
||||
|
||||
/// OSLog-Subsystem für App-Logger, üblich `ev.mana.<app>`. Default
|
||||
/// ist `keychainService` (der schon der Konvention folgt).
|
||||
var logSubsystem: String { get }
|
||||
}
|
||||
|
||||
// MARK: - Default-Implementationen
|
||||
|
||||
public extension ManaAppConfig {
|
||||
/// Default `nil` — Apps ohne Widget/ShareExt müssen nichts setzen.
|
||||
var appGroup: String? { nil }
|
||||
|
||||
/// Default = `keychainService`. Beide folgen heute in allen Apps
|
||||
/// derselben Konvention `ev.mana.<app>`.
|
||||
var logSubsystem: String { keychainService }
|
||||
}
|
||||
|
||||
/// Standard-Implementierung von ``ManaAppConfig``. Apps können diese
|
||||
|
|
@ -29,14 +54,22 @@ public struct DefaultManaAppConfig: ManaAppConfig {
|
|||
public let authBaseURL: URL
|
||||
public let keychainService: String
|
||||
public let keychainAccessGroup: String?
|
||||
public let appGroup: String?
|
||||
public let logSubsystem: String
|
||||
|
||||
public init(
|
||||
authBaseURL: URL,
|
||||
keychainService: String,
|
||||
keychainAccessGroup: String? = nil
|
||||
keychainAccessGroup: String? = nil,
|
||||
appGroup: String? = nil,
|
||||
logSubsystem: String? = nil
|
||||
) {
|
||||
self.authBaseURL = authBaseURL
|
||||
self.keychainService = keychainService
|
||||
self.keychainAccessGroup = keychainAccessGroup
|
||||
self.appGroup = appGroup
|
||||
// Konvention: log-Subsystem = keychainService, falls nicht
|
||||
// explizit anders gewünscht.
|
||||
self.logSubsystem = logSubsystem ?? keychainService
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
Sources/ManaCore/Telemetry/ManaAppLog.swift
Normal file
67
Sources/ManaCore/Telemetry/ManaAppLog.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import Foundation
|
||||
import OSLog
|
||||
|
||||
/// Convenience-Factory für OSLog-Logger gegen ein `ManaAppConfig`.
|
||||
///
|
||||
/// Hintergrund: Jede mana-App hatte vor v1.7.0 ein eigenes `Log.swift`
|
||||
/// mit 4-6 hand-getippten `Logger(subsystem: "ev.mana.<slug>", …)`-
|
||||
/// Aufrufen — der Subsystem-String und die Standard-Kategorien
|
||||
/// (`app`/`auth`/`api`) wiederholten sich 8× identisch. `ManaAppLog`
|
||||
/// kapselt den Subsystem-Lookup gegen die App-Config; Standard-
|
||||
/// Kategorien sind Convenience-Accessoren, app-spezifische gehen über
|
||||
/// ``ManaAppLog/category(_:)``.
|
||||
///
|
||||
/// **Verwendung** (in der App):
|
||||
///
|
||||
/// ```swift
|
||||
/// import ManaCore
|
||||
///
|
||||
/// enum Log {
|
||||
/// private static let mana = ManaAppLog(AppConfig.manaAppConfig)
|
||||
/// static let app = mana.app
|
||||
/// static let auth = mana.auth
|
||||
/// static let api = mana.api
|
||||
/// // App-spezifische Kategorien:
|
||||
/// static let study = mana.category("study")
|
||||
/// static let sync = mana.category("sync")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Die Standard-Kategorien sind bewusst eine kleine Schublade
|
||||
/// (app/auth/api/db/web). Cards/Memoro/Manaspur sollen ihre `study`-/
|
||||
/// `audio`-/`tracking`-Kategorien weiterhin app-spezifisch deklarieren.
|
||||
public struct ManaAppLog: Sendable {
|
||||
public let subsystem: String
|
||||
|
||||
/// Direkter Constructor (für Tests oder andere Subsysteme).
|
||||
public init(subsystem: String) {
|
||||
self.subsystem = subsystem
|
||||
}
|
||||
|
||||
/// Constructor aus einer ``ManaAppConfig``. Nutzt `logSubsystem`
|
||||
/// (Default = `keychainService`, beides üblich `ev.mana.<app>`).
|
||||
public init(_ config: ManaAppConfig) {
|
||||
self.subsystem = config.logSubsystem
|
||||
}
|
||||
|
||||
/// Allgemeiner App-Logger.
|
||||
public var app: Logger { Logger(subsystem: subsystem, category: "app") }
|
||||
|
||||
/// Auth-bezogene Events.
|
||||
public var auth: Logger { Logger(subsystem: subsystem, category: "auth") }
|
||||
|
||||
/// API-/Netzwerk-Calls.
|
||||
public var api: Logger { Logger(subsystem: subsystem, category: "api") }
|
||||
|
||||
/// Datenbank-/SwiftData-/Persistenz-Events.
|
||||
public var db: Logger { Logger(subsystem: subsystem, category: "db") }
|
||||
|
||||
/// Web-/WKWebView-bezogene Events (für Hybrid-Apps).
|
||||
public var web: Logger { Logger(subsystem: subsystem, category: "web") }
|
||||
|
||||
/// App-spezifische Kategorie. Beliebige Strings, weil Console.app
|
||||
/// Kategorien als Free-Text filtert.
|
||||
public func category(_ name: String) -> Logger {
|
||||
Logger(subsystem: subsystem, category: name)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,42 @@ struct ManaAppConfigTests {
|
|||
)
|
||||
#expect(config.keychainAccessGroup == nil)
|
||||
}
|
||||
|
||||
@Test("AppGroup ist optional, default nil")
|
||||
func appGroupDefaultsNil() {
|
||||
let config = DefaultManaAppConfig(
|
||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
||||
keychainService: "ev.mana.cards"
|
||||
)
|
||||
#expect(config.appGroup == nil)
|
||||
}
|
||||
|
||||
@Test("AppGroup wird durchgereicht wenn gesetzt")
|
||||
func appGroupPassedThrough() {
|
||||
let config = DefaultManaAppConfig(
|
||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
||||
keychainService: "ev.mana.cards",
|
||||
appGroup: "group.ev.mana.cards"
|
||||
)
|
||||
#expect(config.appGroup == "group.ev.mana.cards")
|
||||
}
|
||||
|
||||
@Test("LogSubsystem default = keychainService")
|
||||
func logSubsystemDefaultsToKeychainService() {
|
||||
let config = DefaultManaAppConfig(
|
||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
||||
keychainService: "ev.mana.cards"
|
||||
)
|
||||
#expect(config.logSubsystem == "ev.mana.cards")
|
||||
}
|
||||
|
||||
@Test("LogSubsystem kann explizit überschrieben werden")
|
||||
func logSubsystemOverride() {
|
||||
let config = DefaultManaAppConfig(
|
||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
||||
keychainService: "ev.mana.cards",
|
||||
logSubsystem: "ev.mana.cards.debug"
|
||||
)
|
||||
#expect(config.logSubsystem == "ev.mana.cards.debug")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
Tests/ManaCoreTests/ManaAppLogTests.swift
Normal file
53
Tests/ManaCoreTests/ManaAppLogTests.swift
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
@testable import ManaCore
|
||||
|
||||
@Suite("ManaAppLog")
|
||||
struct ManaAppLogTests {
|
||||
@Test("Init aus Config nimmt logSubsystem")
|
||||
func initFromConfig() {
|
||||
let config = DefaultManaAppConfig(
|
||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
||||
keychainService: "ev.mana.cards"
|
||||
)
|
||||
let log = ManaAppLog(config)
|
||||
#expect(log.subsystem == "ev.mana.cards")
|
||||
}
|
||||
|
||||
@Test("Explizit gesetztes logSubsystem überschreibt keychainService")
|
||||
func initFromConfigWithCustomSubsystem() {
|
||||
let config = DefaultManaAppConfig(
|
||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
||||
keychainService: "ev.mana.cards",
|
||||
logSubsystem: "ev.mana.cards.test"
|
||||
)
|
||||
let log = ManaAppLog(config)
|
||||
#expect(log.subsystem == "ev.mana.cards.test")
|
||||
}
|
||||
|
||||
@Test("Direkter String-Constructor")
|
||||
func initFromString() {
|
||||
let log = ManaAppLog(subsystem: "ev.mana.test")
|
||||
#expect(log.subsystem == "ev.mana.test")
|
||||
}
|
||||
|
||||
@Test("Standard-Kategorien sind erreichbar")
|
||||
func standardCategoriesExist() {
|
||||
let log = ManaAppLog(subsystem: "ev.mana.test")
|
||||
// Logger ist nicht Equatable, aber wir können Existenz prüfen
|
||||
// indem wir den Wert binden — Compile genügt.
|
||||
let _ = log.app
|
||||
let _ = log.auth
|
||||
let _ = log.api
|
||||
let _ = log.db
|
||||
let _ = log.web
|
||||
}
|
||||
|
||||
@Test("category() liefert beliebige Custom-Kategorie")
|
||||
func customCategory() {
|
||||
let log = ManaAppLog(subsystem: "ev.mana.test")
|
||||
let _ = log.category("study")
|
||||
let _ = log.category("tracking")
|
||||
let _ = log.category("llm")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue