Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Documentation/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,9 @@ characteristics in ways the diarizer's clustering wasn't tuned for.
by anything that hits ANE in a hot loop (TDT decoder, VAD, segmentation,
embedding).
- `AppLogger` — category-based `os.Logger` wrapper. One logger per
component.
component. Host apps can set `AppLogger.minimumLevel` and
`AppLogger.mirrorsToConsole` at startup (e.g. to keep transcript-bearing
debug lines off stderr in Debug builds).
- `MLModelConfigurationUtils` — default `MLComputeUnits` selection,
CI overrides.
- `ModelHub` (+ `Shared/Download/` primitives: HFClient, RetryPolicy, HFTreeLister, FileDownloader, ModelCache, ProgressReporter) — HuggingFace fetch + caching + token resolution.
Expand Down
77 changes: 56 additions & 21 deletions Sources/FluidAudio/Shared/AppLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,55 @@ public struct AppLogger: Sendable {
/// Note: Set this before creating any logger instances.
nonisolated(unsafe) public static var defaultSubsystem: String = "com.fluidinference"

public enum Level: Int, Sendable {
public enum Level: Int, Sendable, Comparable {
case debug = 0
case info
case notice
case warning
case error
case fault

public static func < (lhs: Level, rhs: Level) -> Bool {
lhs.rawValue < rhs.rawValue
}
}

struct Configuration: Sendable, Equatable {
var minimumLevel: Level = .debug
var mirrorsToConsole: Bool = true
}

private static let configuration = OSAllocatedUnfairLock(initialState: Configuration())

/// Messages below this level are dropped from every sink (console and unified log).
/// Defaults to `.debug`. Set once at startup, e.g. `.warning` to keep ASR debug
/// lines that contain transcript text out of Debug-build output.
public static var minimumLevel: Level {
get { configuration.withLock { $0.minimumLevel } }
set { configuration.withLock { $0.minimumLevel = newValue } }
}

/// When `false`, nothing is written to stderr; messages go to the unified log only
/// (message text is private there unless a debugger is attached). Defaults to `true`.
public static var mirrorsToConsole: Bool {
get { configuration.withLock { $0.mirrorsToConsole } }
set { configuration.withLock { $0.mirrorsToConsole = newValue } }
}

struct Route: Equatable {
var osLog: Bool
var console: Bool
}

static func route(for level: Level) -> Route {
let config = configuration.withLock { $0 }
guard level >= config.minimumLevel else { return Route(osLog: false, console: false) }
#if DEBUG
// Debug builds mirror everything to the console instead of os_log.
return config.mirrorsToConsole ? Route(osLog: false, console: true) : Route(osLog: true, console: false)
#else
return Route(osLog: true, console: config.mirrorsToConsole && level >= .warning)
#endif
}

private let osLogger: Logger
Expand Down Expand Up @@ -62,27 +104,20 @@ public struct AppLogger: Sendable {

// MARK: - Console Mirroring
private func log(_ level: Level, _ message: String) {
#if DEBUG
logToConsole(level, message)
#else
switch level {
case .debug:
osLogger.debug("\(message)")
case .info:
osLogger.info("\(message)")
case .notice:
osLogger.notice("\(message)")
case .warning:
osLogger.warning("\(message)")
logToConsole(level, message) // Also log warnings to console in release
case .error:
osLogger.error("\(message)")
logToConsole(level, message) // Also log errors to console in release
case .fault:
osLogger.fault("\(message)")
logToConsole(level, message) // Also log faults to console in release
let route = AppLogger.route(for: level)
if route.osLog {
switch level {
case .debug: osLogger.debug("\(message)")
case .info: osLogger.info("\(message)")
case .notice: osLogger.notice("\(message)")
case .warning: osLogger.warning("\(message)")
case .error: osLogger.error("\(message)")
case .fault: osLogger.fault("\(message)")
}
}
if route.console {
logToConsole(level, message)
}
#endif
}

private func logToConsole(_ level: Level, _ message: String) {
Expand Down
60 changes: 60 additions & 0 deletions Tests/FluidAudioTests/Shared/AppLoggerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import XCTest

@testable import FluidAudio

final class AppLoggerTests: XCTestCase {

private var savedMinimumLevel: AppLogger.Level = .debug
private var savedMirrorsToConsole = true

override func setUp() {
super.setUp()
savedMinimumLevel = AppLogger.minimumLevel
savedMirrorsToConsole = AppLogger.mirrorsToConsole
}

override func tearDown() {
AppLogger.minimumLevel = savedMinimumLevel
AppLogger.mirrorsToConsole = savedMirrorsToConsole
super.tearDown()
}

func testLevelOrdering() {
XCTAssertLessThan(AppLogger.Level.debug, .info)
XCTAssertLessThan(AppLogger.Level.info, .notice)
XCTAssertLessThan(AppLogger.Level.notice, .warning)
XCTAssertLessThan(AppLogger.Level.warning, .error)
XCTAssertLessThan(AppLogger.Level.error, .fault)
}

func testMinimumLevelDropsLowerLevelsFromAllSinks() {
AppLogger.minimumLevel = .warning
let dropped = AppLogger.Route(osLog: false, console: false)
XCTAssertEqual(AppLogger.route(for: .debug), dropped)
XCTAssertEqual(AppLogger.route(for: .info), dropped)
XCTAssertEqual(AppLogger.route(for: .notice), dropped)
XCTAssertNotEqual(AppLogger.route(for: .warning), dropped)
XCTAssertNotEqual(AppLogger.route(for: .fault), dropped)
}

func testMirrorsToConsoleOffNeverWritesConsole() {
AppLogger.mirrorsToConsole = false
for level: AppLogger.Level in [.debug, .info, .notice, .warning, .error, .fault] {
let route = AppLogger.route(for: level)
XCTAssertFalse(route.console, "\(level) reached console")
XCTAssertTrue(route.osLog, "\(level) missing from os_log")
}
}

func testDefaultRouting() {
AppLogger.minimumLevel = .debug
AppLogger.mirrorsToConsole = true
#if DEBUG
XCTAssertEqual(AppLogger.route(for: .debug), AppLogger.Route(osLog: false, console: true))
XCTAssertEqual(AppLogger.route(for: .error), AppLogger.Route(osLog: false, console: true))
#else
XCTAssertEqual(AppLogger.route(for: .debug), AppLogger.Route(osLog: true, console: false))
XCTAssertEqual(AppLogger.route(for: .error), AppLogger.Route(osLog: true, console: true))
#endif
}
}
Loading