Skip to content

Repository files navigation

Lily

"Make your log line yours. Just let it compose."

A Swift-based library for composable logging, built on swift-log. It helps you shape how they look, structured exactly the way you want.

Formatter

A template that defines the shape of a log line — configure it once and it applies to every event.

import Lily

let formatter = LogFormatter([
    .timestamp,
    " ",
    .level {
        render, _ in
        render().padding(toLength: 8, withPad: " ", startingAt: 0)
    },
    .when({ !$0.label.isEmpty }, then: [" ", .label]),
    " ",
    .message,
    " ",
    .group(["[", .source, "]"]),
    .when({ $0.event.metadata?.isEmpty == false }, then: [": ", .metadata]),
])

StreamLogHandler.standardOutput(label: label, formatter: formatter)
2026-07-29T01:35:23+0700 info     lily Compiling Lily with 47 files [LilyDemo]: target=Lily
2026-07-29T01:35:23+0700 warning  lily Value 'temp' never mutated, use 'let' [LilyDemo]
2026-07-29T01:35:23+0700 error    lily No such module 'MissingDependency' [LilyDemo]: module=MissingDependency
2026-07-29T01:35:23+0700 critical lily Segfault at 0x7ffeebad, address not mapped [LilyDemo]

LogFormatter.standard produces output that matches swift-log's StreamLogHandler default format.

StreamLogHandler.standardOutput(label: label, formatter: .standard)
Components — all available log line building blocks
Component Description
.timestamp The formatted timestamp string prepared by the handler
.level The log level (e.g. info, warning, error)
.label The logger label (e.g. com.example.MyApp)
.message The log message
.metadata All metadata key-value pairs
.metadata(including:) Only selected metadata keys
.metadata(excluding:) All metadata except specified keys
.metadata(key:) A single metadata value by key
.source The swift-log event source
.file The call-site file path
.function The call-site function name
.line The call-site line number
.literal("...") Static text
.group([...]) Child components rendered consecutively
.joined([...], separator:) Non-empty children joined by separator
.when(_:then:) Child components rendered only when predicate is true

Each component also supports a formattedBy: variant for custom formatting closures.

Add color with Rainbow
import Rainbow

let formatter = LogFormatter([
    .timestamp { render, _ in render().dim },
    " ",
    .level { render, context in
        let level = render().padding(toLength: 8, withPad: " ", startingAt: 0).uppercased()
        return switch context.event.level {
        case .debug:    level.blue
        case .info:     level.cyan
        case .warning:  level.yellow
        case .error:    level.red
        case .critical: level.onRed
        default:        level.dim
        }
    },
    .when(
        { !$0.label.isEmpty },
        then: [
            " ",
            .label { render, context in
                let label = render() // handle color support yourself without Rainbow
                return context.supportsColor ? "\u{1B}[35m\(label)\u{1B}[0m" : label
            },
        ]
    ),
    " ",
    .message,
    " ",
    .group(["[", .source, "]"]),
    " ",
    .when({ $0.event.metadata?.isEmpty == false }, then: [": ", .metadata]),
])

StreamLogHandler.standardOutput(label: label, formatter: formatter)

Colorized log output

Each component accepts an optional formattedBy: closure with access to the rendering context — letting you apply ANSI color, reorder fields, or transform values however you like.

Filter

A named predicate in a handler's pipeline — each event either passes through or gets dropped.

let filter = LogFilter(name: "min-level") { event in
    event.level >= .warning ? event : nil
}

var handler = StreamLogHandler.standardOutput(label: label)
handler.addFilter(filter)

Filters run in the order they were added. If any filter returns nil, the event is dropped immediately.

addFilter(_:) is a no-op and returns false if a filter with the same name already exists.

Reusable filter structs
struct NoiseSuppressor: LogFiltering {
    var name: String { "discord-kit-noise" }

    func filter(_ event: LogEvent) -> LogEvent? {
        guard case .warning = event.level,
              event.source == "DiscordKit",
              event.message.description.contains("referencing an unknown")
        else { return event }
        return nil
    }
}

let handler = StreamLogHandler.standardOutput(
    label: label,
    filters: [NoiseSuppressor()]
)

Two approaches — LogFilter for quick one-off closures, LogFiltering for reusable filter structs.

Documentation

You can find reference documentation and user guides for Lily here

Installation

// Package.swift
dependencies: [
    .package(url: "https://github.com/staciax/lily.git", from: "0.1.0")
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "Lily", package: "lily"),
    ])
]

Inspiration

Backstory

Lily started as a quiet little logging project for a Swift programming course — nothing fancy, just a proof of concept. That early version still lives at lilac-v0/.../Logging.swift, the seed for a lot of what came later.

License

Apache 2.0 — see LICENSE.

Releases

Contributors

Languages