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
6 changes: 4 additions & 2 deletions Sources/ConsoleLogger/ConsoleLogger+bootstrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ extension ConsoleLogger {
/// - metadata: Extra metadata to log with all messages. This defaults to an empty dictionary.
/// - metadataProvider: The metadata provider to bootstrap the logging system with.
/// - fragment: The logger fragment which will be used to build the logged messages.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
public static func bootstrap(
printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(),
level: Logger.Level = .info,
metadata: Logger.Metadata = [:],
metadataProvider: Logger.MetadataProvider? = nil,
@LoggerFragmentBuilder fragment: () -> T
@LoggerFragmentBuilder<0> fragment: () -> T
) {
self.bootstrap(fragment: fragment(), printer: printer, level: level, metadata: metadata, metadataProvider: metadataProvider)
}
Expand Down Expand Up @@ -102,12 +103,13 @@ extension ConsoleLogger {
/// - metadata: Extra metadata to log with all messages. This defaults to an empty dictionary.
/// - metadataProvider: The metadata provider to bootstrap the logging system with.
/// - fragment: The logger fragment which will be used to build the logged messages.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
public static func bootstrapWithConfigReader(
printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(),
config: ConfigReader = ConfigReader(providers: [CommandLineArgumentsProvider(), EnvironmentVariablesProvider()]),
metadata: Logger.Metadata = [:],
metadataProvider: Logger.MetadataProvider? = nil,
@LoggerFragmentBuilder fragment: () -> T
@LoggerFragmentBuilder<0> fragment: () -> T
) {
self.bootstrapWithConfigReader(
fragment: fragment(),
Expand Down
6 changes: 4 additions & 2 deletions Sources/ConsoleLogger/ConsoleLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,14 @@ public struct ConsoleLogger<T: LoggerFragment>: LogHandler, Sendable {
/// - metadata: Extra metadata to log with the message. Defaults to an empty dictionary.
/// - metadataProvider: The metadata provider to use for this logger. Defaults to `nil`.
/// - fragment: The ``LoggerFragment`` this logger outputs through.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
public init(
printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(),
label: String,
level: Logger.Level = .debug,
metadata: Logger.Metadata = [:],
metadataProvider: Logger.MetadataProvider? = nil,
@LoggerFragmentBuilder fragment: () -> T
@LoggerFragmentBuilder<0> fragment: () -> T
) {
self.fragment = fragment()
self.printer = printer
Expand Down Expand Up @@ -115,13 +116,14 @@ public struct ConsoleLogger<T: LoggerFragment>: LogHandler, Sendable {
/// - metadata: Extra metadata to log with the message. This defaults to an empty dictionary.
/// - metadataProvider: The metadata provider to use for this logger. This defaults to `nil`.
/// - fragment: The ``LoggerFragment`` this logger outputs through.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
public init(
printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(),
label: String,
config: ConfigReader,
metadata: Logger.Metadata = [:],
metadataProvider: Logger.MetadataProvider? = nil,
@LoggerFragmentBuilder fragment: () -> T
@LoggerFragmentBuilder<0> fragment: () -> T
) {
self.fragment = fragment()
self.printer = printer
Expand Down
1 change: 0 additions & 1 deletion Sources/ConsoleLogger/Docs.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ A `SwiftLog` `LogHandler` implementation for customizable logging to a console.

- ``LoggerFragment``
- ``LoggerFragmentBuilder``
- ``LoggerSpacedFragmentBuilder``
- ``FragmentOutput``
- ``IfMaxLevelFragment``
- ``AndFragment``
Expand Down
9 changes: 6 additions & 3 deletions Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,10 @@ public struct SeparatorFragment<T: LoggerFragment>: LoggerFragment {
public func write(_ record: inout LogRecord, to output: inout FragmentOutput) {
if output.needsSeparator {
if self.fragment.hasContent(record: &record) {
output.needsSeparator = false
output += self.literal
if !self.literal.isEmpty {
output.needsSeparator = false
output += self.literal
}
}
}

Expand Down Expand Up @@ -352,10 +354,11 @@ public struct TimestampFragment<S: TimestampSource>: LoggerFragment {
}

/// A fragment that wraps another fragment, automatically separating its components with spaces.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What in this requires adding availability annotations? And doesn't this affect anyone using a logger fragment?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's the use of integer generics that requires the annotation. Which makes it problematic IMO; I think Tahoe is too strict a requirement as of yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean it's fine if it's scoped to just these APIs and the rest is fine which it looks like, but I'm not 100%.

Also Vapor 5 will be 26+ only given the Span usage so I'm happy to just bump that if necessary, but given the logger is used for more than Vapor I'd like to try and make it as permissive as possible

Copy link
Copy Markdown
Member Author

@fpseverino fpseverino Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's for the integer generics. Tahoe would be required only for the LoggerFragment DSL though, the old API would still be available on older macOS versions.

Also, I guess macOS 27 would already be out when we release the definitive v5 of ConsoleKit.

public struct SpacedFragment<T: LoggerFragment>: LoggerFragment {
public let fragment: T

public init(@LoggerSpacedFragmentBuilder _ content: () -> T) {
public init(@LoggerFragmentBuilder<1> _ content: () -> T) {
self.fragment = content()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
/// A result builder for creating logger fragments in a declarative way.
///
/// This allows you to build complex logger fragment combinations using Swift's result builder syntax.
///
/// You can add spaces between fragments by specifying the number of spaces as the generic parameter.
/// For example, `@LoggerFragmentBuilder<1>` will add a single space between fragments,
/// while `@LoggerFragmentBuilder<0>` will not add any spaces.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@resultBuilder
public enum LoggerFragmentBuilder {
public enum LoggerFragmentBuilder<let spaces: Int> {
/// Build an expression from a single logger fragment.
public static func buildExpression<F: LoggerFragment>(_ fragment: F) -> F {
fragment
Expand Down Expand Up @@ -32,8 +37,8 @@ public enum LoggerFragmentBuilder {
public static func buildPartialBlock<F1: LoggerFragment, F2: LoggerFragment>(
accumulated: F1,
next: F2
) -> AndFragment<F1, F2> {
AndFragment(accumulated, next)
) -> AndFragment<F1, SeparatorFragment<F2>> {
AndFragment(accumulated, next.separated(String(repeating: " ", count: spaces)))
}

/// Handle optional fragments using an optional wrapper.
Expand All @@ -53,6 +58,6 @@ public enum LoggerFragmentBuilder {

/// Build an array of fragments using a custom ``ArrayFragment``.
public static func buildArray<F: LoggerFragment>(_ fragments: [F]) -> ArrayFragment<F> {
ArrayFragment(fragments)
ArrayFragment(fragments, separator: String(repeating: " ", count: spaces))
}
}

This file was deleted.

73 changes: 73 additions & 0 deletions Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,55 @@ import Testing

@Suite("LoggerFragmentBuilder Tests")
struct LoggerFragmentBuilderTests {
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("LoggerFragmentBuilder")
func loggerFragmentBuilder() throws {
let printer = TestingConsoleLoggerPrinter()

@LoggerFragmentBuilder<1>
var fragment: some LoggerFragment {
"Test"
LabelFragment()
LevelFragment()
MessageFragment()
MetadataFragment()
SourceLocationFragment()
}

let logger = Logger(label: "codes.vapor.console") { label in
ConsoleLogger(fragment: fragment, printer: printer, label: label)
}

logger.info("Test message", metadata: ["key": "value"], line: 1)

#expect(printer.testOutputQueue.first == "Test [ codes.vapor.console ] [ INFO ] Test message [key: value] (\(#fileID):1)")
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("LoggerFragmentBuilder with zero spaces")
func loggerFragmentBuilderZeroSpaces() throws {
let printer = TestingConsoleLoggerPrinter()

@LoggerFragmentBuilder<0>
var fragment: some LoggerFragment {
"Test"
LabelFragment()
LevelFragment()
MessageFragment()
MetadataFragment()
SourceLocationFragment()
}

let logger = Logger(label: "codes.vapor.console") { label in
ConsoleLogger(fragment: fragment, printer: printer, label: label)
}

logger.info("Test message", metadata: ["key": "value"], line: 1)

#expect(printer.testOutputQueue.first == "Test[ codes.vapor.console ][ INFO ]Test message[key: value](\(#fileID):1)")
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("Simple Fragment")
func simpleFragment() throws {
let printer = TestingConsoleLoggerPrinter()
Expand All @@ -23,6 +72,7 @@ struct LoggerFragmentBuilderTests {
#expect(printer.testOutputQueue.first == "ConsoleLogger [ codes.vapor.console ] [ INFO ] Test message")
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("Conditional Fragment", arguments: [true, false])
func conditionalFragment(includeTimestamp: Bool) throws {
let printer = TestingConsoleLoggerPrinter()
Expand All @@ -47,6 +97,7 @@ struct LoggerFragmentBuilderTests {
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("Array Fragment")
func arrayFragment() throws {
let printer = TestingConsoleLoggerPrinter()
Expand All @@ -67,6 +118,7 @@ struct LoggerFragmentBuilderTests {
#expect(printer.testOutputQueue.first == "[PREFIX1] [ INFO ] [PREFIX2] [ INFO ] Test message")
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("Empty Block")
func emptyBlock() throws {
let printer = TestingConsoleLoggerPrinter()
Expand All @@ -81,6 +133,7 @@ struct LoggerFragmentBuilderTests {
#expect(printer.testOutputQueue.first == "")
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("Complex Conditional Fragment", arguments: [Logger.Level.error, .warning, .info])
func complexConditionalFragment(level: Logger.Level) throws {
let printer = TestingConsoleLoggerPrinter()
Expand Down Expand Up @@ -110,6 +163,7 @@ struct LoggerFragmentBuilderTests {
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("Default built with LoggerFragmentBuilder")
func defaultFragment() throws {
let loggerBuilderPrinter = TestingConsoleLoggerPrinter()
Expand All @@ -136,4 +190,23 @@ struct LoggerFragmentBuilderTests {

#expect(loggerBuilderPrinter.testOutputQueue[0] == defaultLoggerPrinter.testOutputQueue[0])
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *)
@Test("Empty separator does not consume needsSeparator")
func emptySeparator() throws {
let printer = TestingConsoleLoggerPrinter()
let logger = Logger(label: "codes.vapor.console") { label in
ConsoleLogger(printer: printer, label: label) {
"Hello"
LevelFragment().separated("")
MessageFragment().separated(" ")
}
}

logger.info("Test message")

// `.separated("")` should not insert any text but also should not consume the `needsSeparator` flag,
// so the next `.separated(" ")` still inserts a space.
#expect(printer.testOutputQueue.first == "Hello[ INFO ] Test message")
}
}
Loading