Skip to content

Commit

Permalink
streams
Browse files Browse the repository at this point in the history
  • Loading branch information
samdeane committed Sep 27, 2024
1 parent bbdc399 commit 2b6f580
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 0 deletions.
40 changes: 40 additions & 0 deletions Sources/Logger/Handler/BasicHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane on 27/09/24.
// All code (c) 2024 - present day, Elegant Chaos Limited.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

public actor BasicHandler: Handler {
public typealias Logger = @Sendable (Sendable, Context, BasicHandler) async -> Void

public let name: String
let showName: Bool
let showSubsystem: Bool
let logger: Logger

public init(
_ name: String, showName: Bool = true, showSubsystem: Bool = false, logger: @escaping Logger
) {
self.name = name
self.showName = showName
self.showSubsystem = showSubsystem
self.logger = logger
}

/// Log something.
public func log(_ value: Sendable, context: Context) async { await logger(value, context, self) }

/// Calculate a text tag indicating the context.
/// Provided as a utility for logger callbacks to use as they need.
internal func tag(for context: Context) -> String {
let channel = context.channel
if showName, showSubsystem {
return "[\(channel.subsystem).\(channel.name)] "
} else if showName {
return "[\(channel.name)] "
} else if showSubsystem {
return "[\(channel.subsystem)] "
} else {
return ""
}
}
}
14 changes: 14 additions & 0 deletions Sources/Logger/Handler/Handler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane, 15/02/2018.
// All code (c) 2018 - present day, Elegant Chaos Limited.
// For licensing terms, see http://elegantchaos.com/license/liberal/.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

/// Something responsible for sending log output somewhere.
/// Examples might include printing the output with NSLog/print/OSLog,
/// writing it to disk, or sending it down a pipe or network stream.

public protocol Handler: Sendable {
var name: String { get }
func log(_ value: Sendable, context: Context) async
}
16 changes: 16 additions & 0 deletions Sources/Logger/Handler/NSLogHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane on 27/09/24.
// All code (c) 2024 - present day, Elegant Chaos Limited.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

#if os(macOS) || os(iOS)
import Foundation

/// Outputs log messages using NSLog()
let nslogHandler = BasicHandler("nslog") {
value, context, handler in
let tag = handler.tag(for: context)
NSLog("\(tag)\(value)")
}

#endif
40 changes: 40 additions & 0 deletions Sources/Logger/Handler/OSLogHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane, 27/02/2018.
// All code (c) 2018 - present day, Elegant Chaos Limited.
// For licensing terms, see http://elegantchaos.com/license/liberal/.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)

import os

/// Outputs log messages using os_log().
public actor OSLogHandler: Handler {
/// Table of logs - one for each channel.
var logTable: [Channel: OSLog] = [:]
public let name = "oslog"
public func log(_ value: Sendable, context: Context) async {
let channel = context.channel
let log = logTable[
channel, default: OSLog(subsystem: channel.subsystem, category: channel.name)]

let message = String(describing: value)
let dso = UnsafeRawPointer(bitPattern: context.dso)
os_log("%{public}@", dso: dso, log: log, type: .default, message)
}

public func run(_ stream: LogSequence) async {
for await item in stream {
let channel = item.context.channel
let log = logTable[
channel, default: OSLog(subsystem: channel.subsystem, category: channel.name)]

let message = String(describing: item.value)
let dso = UnsafeRawPointer(bitPattern: item.context.dso)
os_log("%{public}@", dso: dso, log: log, type: .default, message)

}
}
}

#endif
10 changes: 10 additions & 0 deletions Sources/Logger/Handler/PrintHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane on 27/09/24.
// All code (c) 2024 - present day, Elegant Chaos Limited.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

/// Outputs log messages using swift's print() function.
let printHandler = BasicHandler("print") { value, context, handler in
let tag = handler.tag(for: context)
print("\(tag)\(value)")
}
62 changes: 62 additions & 0 deletions Sources/Logger/Handler/StreamHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane on 27/09/24.
// All code (c) 2024 - present day, Elegant Chaos Limited.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

public struct LoggedItem: Sendable {
let value: Sendable
let context: Context
}

/// An async stream of logged items.
public typealias LogStream = AsyncThrowingStream<any Sendable, any Error>
public typealias LogStream2 = AsyncStream<LoggedItem>

/// Handler which outputs the logged items to an async stream.
public actor StreamHandler: Handler {
public init(
_ name: String,
continuation: AsyncThrowingStream<any Sendable, Error>.Continuation
) {
self.name = name
self.continuation = continuation
}

/// Name of the stream.
public let name: String

/// Continuation to yield logged items to.
let continuation: AsyncThrowingStream<Sendable, Error>.Continuation

/// Log an item.
public func log(_ value: Sendable, context: Context) async {
print("logged \(value)")
continuation.yield(value)
}

public func finish() {
continuation.finish()
}
}

public struct LogSequence: AsyncSequence, Sendable {
public typealias AsyncIterator = LogStream2.Iterator
public typealias Element = LoggedItem

var stream: LogStream2!
var continuation: LogStream2.Continuation!

public init() {
self.stream = LogStream2 { continuation in
self.continuation = continuation
}
}

public func makeAsyncIterator() -> LogStream2.Iterator {
stream.makeAsyncIterator()
}

func log(_ item: LoggedItem) {
continuation.yield(item)
}
}
29 changes: 29 additions & 0 deletions Sources/Logger/Settings/ManagerSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane on 20/07/22.
// All code (c) 2022 - present day, Elegant Chaos Limited.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

import Foundation

/// Protocol for reading/writing settings store by the manager.
///
/// This is generalised into a protocol mostly to make it easier to
/// inject settings during testing.

public protocol ManagerSettings {
/// The identifiers of the channels that should be enabled.
var enabledChannelIDs: Set<Channel.ID> { get }

/// Store the identifiers of the channels that should be enabled.
func saveEnabledChannelIDs(_ ids: Set<Channel.ID>)

/// Strip any settings-related command line arguments.
func removeLoggingOptions(fromCommandLineArguments arguments: [String]) -> [String]
}

extension ManagerSettings {
/// Store the channels that should be enabled.
public func saveEnabledChannels(_ channels: Manager.Channels) {
saveEnabledChannelIDs(Set(channels.map(\.id)))
}
}
128 changes: 128 additions & 0 deletions Sources/Logger/Settings/UserDefaultsManagerSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane on 20/07/22.
// All code (c) 2022 - present day, Elegant Chaos Limited.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

import Foundation

/// Implementation of ManagerSettings that uses the UserDefaults system.
/// The default Manager.shared instance uses one of these to read/write
/// settings, and in normal situations it should be all you need.

struct UserDefaultsManagerSettings: ManagerSettings {
let defaults: UserDefaults

init(defaults: UserDefaults = UserDefaults.standard) {
self.defaults = defaults
setup()
}

/**
Calculate the list of enabled channels.
This is determined by two settings: `logsKey` and `persistentLogsKey`,
both of which contain comma-delimited strings.
The persistentLogs setting contains the names of all the channels that were
enabled last time. This is expected to be read from the user defaults.
The logs setting contains modifiers, and if present, is expected to have
been supplied on the command line.
*/

mutating func setup() {
let existingChannels = enabledChannelIDs
let modifiers = defaults.string(forKey: .logsKey) ?? ""
let updatedChannels = Self.updateChannels(existingChannels, applyingModifiers: modifiers)

// persist any changes for the next launch
saveEnabledChannelIDs(updatedChannels)

// we don't want the modifiers to persist between launches, so we clear them out after reading them
defaults.set("", forKey: .logsKey)
}

/**
Update a set of enabled channels, using a list of channel modifiers.
Items in the modifiers can be in two forms:
- "name1,-name2,+name3": *modifies* the list by enabling/disabling named channels
- "=name1,name2,name3": *resets* the list to the named channels
Note that `+name` is a synonym for `name` in the first form - there just for symmetry.
Note also that if any item in the list begins with `=`, the second form is used and the list is reset.
*/

static func updateChannels(_ channels: Set<Channel.ID>, applyingModifiers modifiers: String) -> Set<Channel.ID> {
var updatedChannels = channels
var onlyDeltas = true
var newItems = Set<Channel.ID>()
for item in modifiers.split(separator: ",").map({ $0.trimmingCharacters(in: .whitespaces) }) {
if let first = item.first {
switch first {
case "=":
newItems.insert(Channel.ID(item.dropFirst()))
onlyDeltas = false
case "-":
updatedChannels.remove(Channel.ID(item.dropFirst()))
case "+":
newItems.insert(Channel.ID(item.dropFirst()))
default:
newItems.insert(Channel.ID(item))
}
}
}

if onlyDeltas {
updatedChannels.formUnion(newItems)
} else {
updatedChannels = newItems
}

return updatedChannels
}

var enabledChannelIDs: Set<Channel.ID> {
let s = defaults.string(forKey: .persistentLogsKey)?.split(separator: ",").map { String($0) }
return Set(s ?? [])
}

func saveEnabledChannelIDs(_ ids: Set<Channel.ID>) {
let sortedIDs = ids.sorted().joined(separator: ",")
defaults.set(sortedIDs, forKey: .persistentLogsKey)
}

/**
Returns a copy of the input arguments array which has had any
arguments that we handle stripped out of it.
This is useful for clients that are parsing the command line arguments,
particularly with something like Docopt.
Our options are meant to be semi-hidden, and we don't really want every
client of this library to have to know about all of them, or to have
to document them.
*/

public func removeLoggingOptions(fromCommandLineArguments arguments: [String]) -> [String] {
let key: String = .logsKey
var args: [String] = []
var dropNext = false
for argument in arguments {
if argument == "-\(key)" {
dropNext = true
} else if dropNext {
dropNext = false
} else if !argument.starts(with: "--\(key)=") {
args.append(argument)
}
}
return args
}
}

extension String {
static let persistentLogsKey = "logs-persistent"
static let logsKey = "logs"
}

0 comments on commit 2b6f580

Please sign in to comment.