Skip to content
Open
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 @@ -57,13 +57,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,
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 @@ -114,13 +115,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, *)
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