Skip to content

Commit

Permalink
Implement LoggerFragment system for customizing logger output (#182)
Browse files Browse the repository at this point in the history
* Implement LoggerFragment system for customizing logger output
* Add performance test, and remove now-irrelevant Swift version conditions
* Start adding Sendable
* Update Terminal's report(error:,newline:) method
* Remove conditional ConsoleLogger LogHandler method
* Add a LoggingSystem.bootstrap method for logger fragments
* Add a LoggerSourceFragment, and make TimestampFragment more easily testable
  • Loading branch information
semicoleon authored Sep 18, 2023
1 parent 9a12000 commit dcaea6c
Show file tree
Hide file tree
Showing 40 changed files with 957 additions and 182 deletions.
13 changes: 11 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.6
// swift-tools-version:5.7
import PackageDescription

let package = Package(
Expand All @@ -14,22 +14,31 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.56.0"),
],
targets: [
.target(name: "ConsoleKit", dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "NIOConcurrencyHelpers", package: "swift-nio")
]),
.testTarget(name: "ConsoleKitTests", dependencies: [
.target(name: "ConsoleKit"),
]),
.testTarget(name: "AsyncConsoleKitTests", dependencies: [
.target(name: "ConsoleKit"),
]),
.testTarget(name: "ConsoleKitPerformanceTests", dependencies: [
.target(name: "ConsoleKit")
]),
.executableTarget(name: "ConsoleKitExample", dependencies: [
.target(name: "ConsoleKit"),
]),
.target(name: "ConsoleKitAsyncExample", dependencies: [
.executableTarget(name: "ConsoleKitAsyncExample", dependencies: [
.target(name: "ConsoleKit")
]),
.executableTarget(name: "ConsoleLoggerExample", dependencies: [
.target(name: "ConsoleKit"),
.product(name: "Logging", package: "swift-log")
])
]
)
49 changes: 49 additions & 0 deletions Package@swift-5.9.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// swift-tools-version:5.9
import PackageDescription

let package = Package(
name: "console-kit",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.watchOS(.v6),
.tvOS(.v13),
],
products: [
.library(name: "ConsoleKit", targets: ["ConsoleKit"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.56.0"),
],
targets: [
.target(name: "ConsoleKit", dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "NIOConcurrencyHelpers", package: "swift-nio")
], swiftSettings: [
.enableExperimentalFeature("StrictConcurrency=complete"),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("ForwardTrailingClosures"),
.enableUpcomingFeature("ConciseMagicFile"),
]),
.testTarget(name: "ConsoleKitTests", dependencies: [
.target(name: "ConsoleKit"),
], swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]),
.testTarget(name: "AsyncConsoleKitTests", dependencies: [
.target(name: "ConsoleKit"),
], swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]),
.testTarget(name: "ConsoleKitPerformanceTests", dependencies: [
.target(name: "ConsoleKit")
], swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]),
.executableTarget(name: "ConsoleKitExample", dependencies: [
.target(name: "ConsoleKit"),
], swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]),
.executableTarget(name: "ConsoleKitAsyncExample", dependencies: [
.target(name: "ConsoleKit")
], swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]),
.executableTarget(name: "ConsoleLoggerExample", dependencies: [
.target(name: "ConsoleKit"),
.product(name: "Logging", package: "swift-log")
])
]
)
32 changes: 24 additions & 8 deletions Sources/ConsoleKit/Activity/ActivityBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,41 @@ public protocol ActivityBar: ActivityIndicatorType {

extension ActivityBar {
/// See `ActivityIndicatorType`.
public func outputActivityIndicator(to console: Console, state: ActivityIndicatorState) {
public func outputActivityIndicator(to console: any Console, state: ActivityIndicatorState) {
let bar: ConsoleText
switch state {
case .ready: bar = "[]"
case .active(let tick): bar = renderActiveBar(tick: tick, width: Self.width)
case .active(let tick): bar = renderActiveBar(tick: tick, width: console.activityBarWidth)
case .success: bar = "[Done]".consoleText(.success)
case .failure: bar = "[Failed]".consoleText(.error)
}
console.output(title.consoleText(.plain) + " " + bar)
}
}

/// Defines the width of all `ActivityBar`s in characters.
private var _width: Int = 25

extension ActivityBar {
/// Defines the width of all `ActivityBar`s in characters.
@available(*, deprecated, message: "This value has no effect. Use `console.activityBarWidth` instead.")
public static var width: Int {
get { return _width }
set { _width = newValue}
get { 25 } // deliberately hardcoded value
set { } // deliberately ignore new value
}
}

/// Key type for storing the activity bar width in the `userInfo` of the related `Console` without colliding with end user keys.
struct ActivityBarWidthKey: Hashable, Equatable {
func hash(into hasher: inout Hasher) {
hasher.combine("ConsoleKit.ActivityBarWidthKey")
}
}

extension Console {
public var activityBarWidth: Int {
get {
self.userInfo[AnySendableHashable(ActivityBarWidthKey())] as? Int ?? 25
}

set {
self.userInfo[AnySendableHashable(ActivityBarWidthKey())] = newValue
}
}
}
38 changes: 28 additions & 10 deletions Sources/ConsoleKit/Activity/ActivityIndicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Darwin
#else
import Glibc
#endif
import NIOConcurrencyHelpers

extension ActivityIndicatorType {
/// Creates a new `ActivityIndicator` for this `ActivityIndicatorType`.
Expand All @@ -15,7 +16,7 @@ extension ActivityIndicatorType {
/// - targetQueue: An optional target queue (defaults to `nil`) on which
/// asynchronous updates to the console will be
/// scheduled.
public func newActivity(for console: Console, targetQueue: DispatchQueue? = nil) -> ActivityIndicator<Self> {
public func newActivity(for console: any Console, targetQueue: DispatchQueue? = nil) -> ActivityIndicator<Self> {
return .init(activity: self, console: console, targetQueue: targetQueue)
}
}
Expand All @@ -33,15 +34,23 @@ extension ActivityIndicatorType {
/// // start the loading bar and wait for it to finish
/// try loadingBar.start(on: ...).wait()
///
public final class ActivityIndicator<A> where A: ActivityIndicatorType {
public final class ActivityIndicator<A>: Sendable where A: ActivityIndicatorType {
let _activity: NIOLockedValueBox<A>
/// The generic `ActivityIndicatorType` powering this `ActivityIndicator`.
public var activity: A
public var activity: A {
get {
self._activity.withLockedValue { $0 }
}
set {
self._activity.withLockedValue { $0 = newValue }
}
}

/// The `Console` this `ActivityIndicator` is running on.
private let console: Console
private let console: any Console

/// Current state.
private var state: ActivityIndicatorState
private let state: NIOLockedValueBox<ActivityIndicatorState>

/// The queue on which to handle timer events
private let queue: DispatchQueue
Expand All @@ -50,16 +59,25 @@ public final class ActivityIndicator<A> where A: ActivityIndicatorType {
/// dispatch timer is cancelled.
private let stopGroup: DispatchGroup

private let _timer: NIOLockedValueBox<any DispatchSourceTimer & Sendable>
/// The timer that drives this activity indicator's updates.
private var timer: DispatchSourceTimer
private var timer: any DispatchSourceTimer & Sendable {
get {
self._timer.withLockedValue { $0 }
}

set {
self._timer.withLockedValue { $0 = newValue }
}
}

/// Creates a new `ActivityIndicator`. Use `ActivityIndicatorType.newActivity(for:)`.
init(activity: A, console: Console, targetQueue: DispatchQueue? = nil) {
init(activity: A, console: any Console, targetQueue: DispatchQueue? = nil) {
self.console = console
self.state = .ready
self.activity = activity
self.state = NIOLockedValueBox(.ready)
self._activity = NIOLockedValueBox(activity)
self.queue = DispatchQueue(label: "codes.vapor.consolekit.activityindicator", target: targetQueue)
self.timer = DispatchSource.makeTimerSource(flags: [], queue: self.queue)
self._timer = NIOLockedValueBox(DispatchSource.makeTimerSource(flags: [], queue: self.queue) as! DispatchSource)
self.stopGroup = DispatchGroup()
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/ConsoleKit/Activity/ActivityIndicatorRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// behind calling the `ActivityIndicatorType` for `ActivityIndicatorState` changes.
///
/// See the `ActivityBar` protocol which is based off of this protocol.
public protocol ActivityIndicatorType {
public protocol ActivityIndicatorType: Sendable {
/// Draws / renders this `ActivityIndicatorType` to the `Console` for the supplied `ActivityIndicatorState`.
///
/// This method will be called by the `ActivityIndicator`. The `Console` will have any previous
Expand All @@ -14,5 +14,5 @@ public protocol ActivityIndicatorType {
/// - parameters:
/// - console: `Console` to output this indicator to.
/// - state: State to draw the indicator in, e.g., active, failed.
func outputActivityIndicator(to console: Console, state: ActivityIndicatorState)
func outputActivityIndicator(to console: any Console, state: ActivityIndicatorState)
}
2 changes: 1 addition & 1 deletion Sources/ConsoleKit/Activity/ActivityIndicatorState.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// Possible states to draw / render and `ActivityIndicatorType`.
///
/// See `ActivityIndicatorType`.
public enum ActivityIndicatorState {
public enum ActivityIndicatorState: Sendable {
/// Default state. This is usually never used other than for initialization.
case ready

Expand Down
2 changes: 1 addition & 1 deletion Sources/ConsoleKit/Activity/CustomActivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public struct CustomActivity: ActivityIndicatorType {


/// See `ActivityIndicatorType.outputActivityIndicator(to:state:)`.
public func outputActivityIndicator(to console: Console, state: ActivityIndicatorState) {
public func outputActivityIndicator(to console: any Console, state: ActivityIndicatorState) {
let output: ConsoleText

switch state {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ConsoleKit/Activity/LoadingBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public struct LoadingBar: ActivityBar {
let reverse = Int(tick) % (period * 2) >= period

let increasing = offset
let decreasing = LoadingBar.width - offset - 1
let decreasing = width - offset - 1

let left: Int
let right: Int
Expand Down
4 changes: 2 additions & 2 deletions Sources/ConsoleKit/Activity/ProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public struct ProgressBar: ActivityBar {
public func renderActiveBar(tick: UInt, width: Int) -> ConsoleText {
let current = min(max(currentProgress, 0.0), 1.0)

let left = Int(current * Double(ProgressBar.width))
let right = ProgressBar.width - left
let left = Int(current * Double(width))
let right = width - left

var barComponents: [String] = []
barComponents.append("[")
Expand Down
2 changes: 1 addition & 1 deletion Sources/ConsoleKit/Command/Async/AnyAsyncCommand.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// A type-erased `Command`.
public protocol AnyAsyncCommand {
public protocol AnyAsyncCommand: Sendable {
/// Text that will be displayed when `--help` is passed.
var help: String { get }

Expand Down
8 changes: 4 additions & 4 deletions Sources/ConsoleKit/Command/Async/AsyncCommandGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
///
/// You can create your own `AsyncCommandGroup` if you want to support custom `CommandOptions`.
public protocol AsyncCommandGroup: AnyAsyncCommand {
var commands: [String: AnyAsyncCommand] { get }
var defaultCommand: AnyAsyncCommand? { get }
var commands: [String: any AnyAsyncCommand] { get }
var defaultCommand: (any AnyAsyncCommand)? { get }
}

extension AsyncCommandGroup {
public var defaultCommand: AnyAsyncCommand? {
public var defaultCommand: (any AnyAsyncCommand)? {
return nil
}
}
Expand Down Expand Up @@ -80,7 +80,7 @@ extension AsyncCommandGroup {
context.console.output(" [--help,-h]".consoleText(.success) + "` for more information on a command.")
}

private func commmand(using context: inout CommandContext) throws -> AnyAsyncCommand? {
private func commmand(using context: inout CommandContext) throws -> (any AnyAsyncCommand)? {
if let name = context.input.arguments.popFirst() {
guard let command = self.commands[name] else {
throw CommandError.unknownCommand(name, available: Array(self.commands.keys))
Expand Down
16 changes: 8 additions & 8 deletions Sources/ConsoleKit/Command/Async/AsyncCommands.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/// Represents a top-level group of configured commands. This is usually created by calling `resolve(for:)` on `AsyncCommands`.
public struct AsyncCommands {
/// Top-level available commands, stored by unique name.
public var commands: [String: AnyAsyncCommand]
public var commands: [String: any AnyAsyncCommand]

/// If set, this is the default top-level command that should run if no other commands are specified.
public var defaultCommand: AnyAsyncCommand?
public var defaultCommand: (any AnyAsyncCommand)?

/// If `true`, an `autocomplete` subcommand will be added to any created `AsyncCommandGroup`.
///
Expand All @@ -30,8 +30,8 @@ public struct AsyncCommands {
/// automatically be included in the completion script generation process.
///
public init(
commands: [String: AnyAsyncCommand] = [:],
defaultCommand: AnyAsyncCommand? = nil,
commands: [String: any AnyAsyncCommand] = [:],
defaultCommand: (any AnyAsyncCommand)? = nil,
enableAutocomplete: Bool = false
) {
self.commands = commands
Expand All @@ -49,7 +49,7 @@ public struct AsyncCommands {
/// - name: A unique name for running this command.
/// - isDefault: If `true`, this command will be set as the default command to run when none other are specified.
/// Setting this overrides any previous default commands.
public mutating func use(_ command: AnyAsyncCommand, as name: String, isDefault: Bool = false) {
public mutating func use(_ command: any AnyAsyncCommand, as name: String, isDefault: Bool = false) {
self.commands[name] = command
if isDefault {
self.defaultCommand = command
Expand All @@ -67,7 +67,7 @@ public struct AsyncCommands {
/// - parameters:
/// - help: Optional help messages to include.
/// - returns: An `AsyncCommandGroup` with commands and defaultCommand configured.
public func group(help: String = "") -> AsyncCommandGroup {
public func group(help: String = "") -> any AsyncCommandGroup {
var group = _AsyncGroup(
commands: self.commands,
defaultCommand: self.defaultCommand,
Expand All @@ -85,7 +85,7 @@ public struct AsyncCommands {
}

private struct _AsyncGroup: AsyncCommandGroup {
var commands: [String: AnyAsyncCommand]
var defaultCommand: AnyAsyncCommand?
var commands: [String: any AnyAsyncCommand]
var defaultCommand: (any AnyAsyncCommand)?
let help: String
}
4 changes: 2 additions & 2 deletions Sources/ConsoleKit/Command/CommandContext.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// A type-erased `CommandContext`
public struct CommandContext {
/// The `Console` this command was run on.
public var console: Console
public var console: any Console

/// The parsed arguments (according to declared signature).
public var input: CommandInput
Expand All @@ -10,7 +10,7 @@ public struct CommandContext {

/// Create a new `AnyCommandContext`.
public init(
console: Console,
console: any Console,
input: CommandInput
) {
self.console = console
Expand Down
8 changes: 4 additions & 4 deletions Sources/ConsoleKit/Command/CommandGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
///
/// You can create your own `CommandGroup` if you want to support custom `CommandOptions`.
public protocol CommandGroup: AnyCommand {
var commands: [String: AnyCommand] { get }
var defaultCommand: AnyCommand? { get }
var commands: [String: any AnyCommand] { get }
var defaultCommand: (any AnyCommand)? { get }
}

extension CommandGroup {
public var defaultCommand: AnyCommand? {
public var defaultCommand: (any AnyCommand)? {
return nil
}
}
Expand Down Expand Up @@ -80,7 +80,7 @@ extension CommandGroup {
context.console.output(" [--help,-h]".consoleText(.success) + "` for more information on a command.")
}

private func commmand(using context: inout CommandContext) throws -> AnyCommand? {
private func commmand(using context: inout CommandContext) throws -> (any AnyCommand)? {
if let name = context.input.arguments.popFirst() {
guard let command = self.commands[name] else {
throw CommandError.unknownCommand(name, available: Array(self.commands.keys))
Expand Down
Loading

0 comments on commit dcaea6c

Please sign in to comment.