Skip to content

Commit

Permalink
work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
samdeane committed Sep 13, 2024
1 parent 2d2da33 commit fa9e177
Show file tree
Hide file tree
Showing 13 changed files with 519 additions and 532 deletions.
106 changes: 29 additions & 77 deletions Sources/Logger/Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,6 @@ import Foundation
*/

public actor Channel {
/**
Default log handler which prints to standard out,
without appending the channel details.
*/

public static let stdoutHandler = PrintHandler("default", showName: false, showSubsystem: false)

/**
Default handler to use for channels if nothing else is specified.

On the Mac this is an OSLogHandler, which will log directly to the console without
sending output to stdout/stderr.

On Linux it is a PrintHandler which will log to stdout.
*/

static func initDefaultHandler() -> Handler {
#if os(macOS) || os(iOS)
return OSLogHandler("default")
#else
return stdoutHandler // TODO: should perhaps be stderr instead?
#endif
}

public static let defaultHandler = initDefaultHandler()

/**
Default subsystem if nothing else is specified.
Expand All @@ -49,55 +24,18 @@ public actor Channel {
is used for the subsytem.
*/

static let defaultSubsystem = "com.elegantchaos.logger"

/**
Default log channel that clients can use to log their actual output.
This is intended as a convenience for command line programs which actually want to produce output.
They could of course just use print() for this (producing normal output isn't strictly speaking
the same as logging), but this channel exists to allow output and logging to be treated in a uniform
way.

Unlike most channels, we want this one to default to always being on.
*/

public static let stdout = Channel("stdout", handlers: [stdoutHandler], alwaysEnabled: true)

public let name: String
public let subsystem: String

nonisolated(unsafe) public private(set) var enabled: Bool

public var fullName: String {
"\(subsystem).\(name)"
}
nonisolated(unsafe) public var enabled: Bool

let manager: Manager
var handlers: [Handler] = []

/// MainActor isolated properties for use in the user interface.
/// This object is observable so the UI can watch it for changes
/// to the enabled state of the channel.
@MainActor public class UI: Identifiable, ObservableObject {
@Published public private(set) var enabled: Bool
public weak var channel: Channel!
public let id: String

init(channel: Channel, id: String, enabled: Bool) {
self.channel = channel
self.id = id
self.enabled = enabled
}

func setEnabled(state: Bool) {
enabled = state
}
}

public let ui: UI
static let defaultSubsystem = "com.elegantchaos.logger"

public init(
_ name: String, handlers: @autoclosure () -> [Handler] = [defaultHandler],
_ name: String, handlers: @autoclosure () -> [Handler] = [Manager.defaultHandler],
alwaysEnabled: Bool = false, manager: Manager = Manager.shared
) {
let components = name.split(separator: ".")
Expand All @@ -124,7 +62,6 @@ public actor Channel {
self.enabled = isEnabled

self.handlers = handlers() // TODO: does this need to be a closure any more?
self.ui = UI(channel: self, id: fullName, enabled: isEnabled)
Task {
await manager.register(channel: self)
}
Expand All @@ -135,38 +72,46 @@ public actor Channel {

The logged value is an autoclosure, to avoid doing unnecessary work if the channel is disabled.

If the channel is enabled, we capture the logged value in the calling thread, by evaluating the autoclosure.
We then log the value asynchronously. The log manager serialises the logging, to avoid race conditions.
If the channel is enabled, we capture the logged value, by evaluating the autoclosure.
We then log the value asynchronously.s

Note that reading the `enabled` flag is not serialised, to avoid taking unnecessary locks. It's theoretically
Note that reading the `enabled` flag is not isolated, to avoid taking unnecessary locks. It's theoretically
possible for another thread to be writing to this flag whilst we're reading it, if the channel state is being
changed. Thread sanitizer might flag this up, but it's harmless, and should generally only happen in a debug
setting.
*/

public func log(
_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line,
nonisolated public func log<T>(
_ logged: @autoclosure () -> T, file: StaticString = #file, line: UInt = #line,
column: UInt = #column, function: StaticString = #function
) {
if enabled {
let value = logged()
let context = Context(file: file, line: line, column: column, function: function)
self.handlers.forEach { $0.log(channel: self, context: context, logged: value) }
let value = asSendable(logged)
Task.detached { await self._log(value, context: context) }
}
}

public func debug(
_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line,
public func _log(_ value: Sendable, context: Context) {
for handler in handlers {
Task { await handler.log(channel: self, context: context, logged: value) }
}
}

nonisolated public func debug<T>(
_ logged: @autoclosure () -> T, file: StaticString = #file, line: UInt = #line,
column: UInt = #column, function: StaticString = #function
) {
#if DEBUG
if enabled {
log(logged(), file: file, line: line, column: column, function: function)
let context = Context(file: file, line: line, column: column, function: function)
let value = asSendable(logged)
Task { await self._log(value, context: context) }
}
#endif
}

public func fatal(
nonisolated public func fatal(
_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line,
column: UInt = #column, function: StaticString = #function
) -> Never {
Expand All @@ -176,6 +121,10 @@ public actor Channel {
}
}

extension Channel: Identifiable {
nonisolated public var id: String { "\(subsystem).\(name)" }
}

extension Channel: Hashable {
// For now, we treat channels with the same name as equal,
// as long as they belong to the same manager.
Expand All @@ -187,3 +136,6 @@ extension Channel: Hashable {
(lhs.name == rhs.name) && (lhs.manager === rhs.manager)
}
}

func asSendable<T>(_ value: () -> T) -> Sendable where T: Sendable { value() }
func asSendable<T>(_ value: () -> T) -> Sendable { String(describing: value()) }
98 changes: 98 additions & 0 deletions Sources/Logger/ChannelsWatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane on 13/09/24.
// All code (c) 2024 - present day, Elegant Chaos Limited.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

import Combine
import Foundation

// @MainActor class ChannelsWatcher: Manager.LogObserver, ObservableObject {
// var allChannels = Set<Channel>()
// var enabledChannels = Set<Channel>()

// /**
// Update the enabled/disabled state of one or more channels.
// The change is persisted in the settings, and will be restored
// next time the application runs.
// */

// public func update(channels: [Channel], state: Bool) {
// for channel in channels {
// channel.enabled = state
// let change = channel.enabled ? "enabled" : "disabled"
// print("Channel \(channel.name) \(change).")
// }

// saveChannelSettings()
// postChangeNotification()
// }

// func channelsUpdated(
// _ channels: Manager.Channels, enabled: Manager.Channels, all: Manager.Channels
// ) {
// self.allChannels = all
// self.enabledChannels = enabled
// }

// let manager: Manager

// /**
// Add to our list of registered channels.
// */

// internal func register(channel: Channel) {
// channels.insert(channel)
// scheduleNotification(for: channel)
// }

// /**
// All the channels registered with the manager.

// Channels get registered when they're first used,
// which may not necessarily be when the application first runs.
// */

// public var registeredChannels: [Channel] {
// channels // TODO: expose a ObservableObject copy
// }

// /**
// All the enabled channels.
// */
// public var enabledChannels: [Channel] {
// return channels.filter { await $0.enabled }
// }

// /**
// State of all channels together; useful for the debug UI.
// */
// public enum ChannelsState {
// case allDisabled
// case allEnabled
// case mixed
// }

// /**
// Current state of all channels.
// */

// public var channelsState: ChannelsState {
// let registeredChannels = self.registeredChannels
// let enabledCount = registeredChannels.filter(\.enabled).count
// if enabledCount == registeredChannels.count {
// return .allEnabled
// } else if enabledCount == 0 {
// return .allDisabled
// } else {
// return .mixed
// }
// }

// /**
// Returns a channel with a give name, if we have one.
// */
// public func channel(named name: String) -> Channel? {
// registeredChannels.first(where: { $0.name == name })
// }

// }
35 changes: 19 additions & 16 deletions Sources/Logger/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,25 @@
Encapsulates the context (function, line number, etc) in which the log statement was made.
*/

public struct Context: CustomStringConvertible {
let file: StaticString
let line: UInt
let function: StaticString
let column: UInt
let dso: UnsafeRawPointer
public struct Context: CustomStringConvertible, Sendable {
let file: StaticString
let line: UInt
let function: StaticString
let column: UInt
let dso: UnsafeRawPointer

public init(file: StaticString = #file, line: UInt = #line, column: UInt = #column, function: StaticString = #function, dso: UnsafeRawPointer = #dsohandle) {
self.file = file
self.line = line
self.function = function
self.column = column
self.dso = dso
}
public init(
file: StaticString = #file, line: UInt = #line, column: UInt = #column,
function: StaticString = #function, dso: UnsafeRawPointer = #dsohandle
) {
self.file = file
self.line = line
self.function = function
self.column = column
self.dso = dso
}

public var description: String {
"\(file): \(line),\(column) - \(function)"
}
public var description: String {
"\(file): \(line),\(column) - \(function)"
}
}
19 changes: 19 additions & 0 deletions Sources/Logger/Defaults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
struct Defaults {
/// Default log handler which prints to standard out, without appending the channel details.
@MainActor public static let stdoutHandler = PrintHandler(
"default", showName: false, showSubsystem: false)

/**
Default log channel that clients can use to log their actual output.
This is intended as a convenience for command line programs which actually want to produce output.
They could of course just use print() for this (producing normal output isn't strictly speaking
the same as logging), but this channel exists to allow output and logging to be treated in a uniform
way.

Unlike most channels, we want this one to default to always being on.
*/

@MainActor public static let stdout = Channel(
"stdout", handlers: [stdoutHandler], alwaysEnabled: true)

}
Loading

0 comments on commit fa9e177

Please sign in to comment.