diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..067bbd4 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "17b7149414687671044398e9ba80abb41bea4bc04f2d6ea3ca1b5adbe55af6c1", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "4799286537280063c85a32f09884cfbca301b1a1", + "version" : "602.0.0" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 14db3b3..1b13f34 100755 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,7 @@ // swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. +import CompilerPluginSupport import PackageDescription let package = Package( @@ -19,11 +20,22 @@ let package = Package( targets: ["Scribe"] ) ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0-latest") + ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. + .macro( + name: "ScribeMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), .target( - name: "Scribe" + name: "Scribe", + dependencies: ["ScribeMacros"] ), .testTarget( name: "ScribeTests", diff --git a/README.md b/README.md index d322a50..0b330c6 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,25 @@ extension LogCategory { - Each category maps to its own `Logger` instance under the same subsystem, so Console and Xcode show first-class category filters without extra configuration. +### @Loggable Macro + +Automatically generates a `log` property for classes, structs, and enums. + +```swift +@Loggable +struct TokenManager { + func refresh() { + log.info("Refreshing token") + } +} +``` + +**Options:** +- `@Loggable` — uses the type name as the category +- `@Loggable("CustomName")` — uses a custom category name +- `@Loggable(category: .network)` — uses an existing `LogCategory` +- `@Loggable(style: .static)` — generates a static `log` property instead of instance + ### LogManager Core logger with configuration and sinks. @@ -112,13 +131,18 @@ Core logger with configuration and sinks. - `minimumLevel: LogLevel` — threshold (read/write) - `configuration: LogConfiguration` — formatting and filtering (read/write) - `sinkCount: Int` — number of registered sinks + - `loggerCacheCount: Int` — number of cached `Logger` instances - **Methods**: - `log(_:level:category:file:function:line:)` — core logging method - `setMinimumLevel(_:)` — async level setter + - `getMinimumLevel(_:)` — async level getter with completion handler - `setConfiguration(_:)` — async configuration setter - - `addSink(_:) -> SinkID` — register a sink, returns ID for removal + - `getConfiguration(_:)` — async configuration getter with completion handler + - `addSink(categories:_:) -> LogSubscription` — register a sink with optional category filter, returns ID for removal - `removeSink(_:)` — remove a specific sink by ID - `removeAllSinks()` — remove all sinks + - `stream(categories:) -> AsyncStream` — create an async stream of log messages with optional category filter + - `clearLoggerCache(completion:)` — clear cached `Logger` instances ### LogConfiguration @@ -129,6 +153,7 @@ Configuration struct for customizing log output. - `includeTimestamp: Bool` — include timestamps (default: `true`) - `includeEmoji: Bool` — include level emoji (default: `true`) - `includeShortCode: Bool` — include level short code like `[DBG]` (default: `false`) +- `includeFileAndLineNumber: Bool` — include source file and line number (default: `true`) - `autoLoggerCacheLimit: Int?` — limit cached auto-generated `Logger` instances (e.g., `#fileID`); `nil` means unbounded; default is 100 - `dateFormat: String` — timestamp format (default: `"yyyy-MM-dd HH:mm:ss.SSSZ"`) @@ -275,8 +300,8 @@ LogManager.shared.clearLoggerCache() ``` - Notes: - - Auto-generated categories (`#fileID`) are cached in an internal `NSCache` with a default cap of 100. - - Custom categories you define (e.g., `LogCategory("APIService")`) are stored in a dictionary and are not evicted. + - Auto-generated categories (`#fileID`) are cached with a default limit of 100. When the limit is reached, the least recently used loggers are removed first. + - Custom categories you define (e.g., `LogCategory("APIService")`) are cached permanently and not removed. ### Combined Configuration @@ -329,6 +354,20 @@ LogManager.shared.removeAllSinks() let count = LogManager.shared.sinkCount ``` +### Streaming + +For async/await, use `stream()` instead of callbacks: + +```swift +let stream = LogManager.shared.stream() + +Task { + for await line in stream { + print("Log:", line) + } +} +``` + ## Threading and Performance - Logging occurs on a dedicated utility queue to minimize call-site blocking. @@ -339,6 +378,7 @@ let count = LogManager.shared.sinkCount ## Best Practices +- Use the `@Loggable` macro to automatically generate a `log` property for your types. - Use categories to group logs by module or feature (e.g., `LogCategory("APIService")`, `LogCategory("Storage")`). - Raise `minimumLevel` in production (e.g., `.info` or `.warning`). - Avoid logging PII or secrets; this package does not perform encryption or redaction. @@ -350,14 +390,14 @@ let count = LogManager.shared.sinkCount ```swift extension LogCategory { static let app = LogCategory("App") - static let apiService = LogCategory("APIService") } +@Loggable final class APIService { func fetchProfile() { - Log.api("GET /v1/profile", category: .apiService) + log.api("GET /v1/profile") // ... network call ... - Log.debug("Decoded Profile(id: 123)", category: .apiService) + log.debug("Decoded Profile(id: 123)") } } @@ -368,7 +408,7 @@ struct MyApp: App { LogManager.shared.minimumLevel = .info let config = LogConfiguration( - enabledCategories: [.app, .apiService], + enabledCategories: [.app, APIService.logCategory], includeTimestamp: true ) LogManager.shared.configuration = config diff --git a/Sources/Scribe/Log+StaticMethods.swift b/Sources/Scribe/Log+StaticMethods.swift new file mode 100644 index 0000000..7c9b694 --- /dev/null +++ b/Sources/Scribe/Log+StaticMethods.swift @@ -0,0 +1,436 @@ +// +// Log+StaticMethods.swift +// Scribe +// +// Created by Kami on 04/02/2025. +// + +import Foundation + +/// Static logging convenience methods providing easy access for all log levels. +/// +/// Use the static methods on this enum to log messages throughout your application. +/// Each method automatically captures source location metadata (file, function, line). +/// +/// ```swift +/// Log.info("User signed in", category: .init("Auth")) +/// Log.error("Failed to fetch data", category: .init("Network")) +/// Log.debug("Processing item \(item.id)") +/// ``` +/// +/// The `category` parameter defaults to the file ID where the log was called, +/// but you can specify custom categories for better filtering and organization. +public extension Log { + // MARK: - Debug & Development + + /// Logs a debug message for development and troubleshooting. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func debug( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .debug, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs a trace message for detailed execution flow tracking. + /// + /// Use for granular debugging that's typically too verbose for regular debug builds. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func trace( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .trace, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs a print-level message as a structured replacement for `Swift.print()`. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func print( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .print, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - General Information + + /// Logs an informational message about general app state or events. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func info( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .info, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs a notice about a notable but non-critical event. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func notice( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .notice, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Warnings & Errors + + /// Logs a warning about a potential issue that doesn't prevent operation. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func warn( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .warning, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs an error that occurred but was handled. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func error( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .error, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs a fatal error that may cause app termination or severe malfunction. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func fatal( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .fatal, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Success & Completion + + /// Logs a successful operation or outcome. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func success( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .success, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs task or operation completion. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func done( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .done, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Network Operations + + /// Logs network-related activity. + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func network( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .network, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs API call activity (requests, responses, errors). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func api( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .api, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Security & Authentication + + /// Logs security-related events (encryption, permissions, access control). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func security( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .security, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs authentication events (login, logout, token refresh). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func auth( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .auth, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Performance & Analytics + + /// Logs performance metrics (timing, memory, resource usage). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func metric( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .metric, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs analytics events (user behavior, feature usage). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func analytics( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .analytics, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - UI & User Interaction + + /// Logs UI events (view lifecycle, layout, rendering). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func ui( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .ui, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs user actions (taps, gestures, input). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func user( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .user, category: category, fileID: fileID, function: function, file: file, line: line) + } + + // MARK: - Database & Storage + + /// Logs database operations (queries, transactions, migrations). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func database( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .database, category: category, fileID: fileID, function: function, file: file, line: line) + } + + /// Logs storage operations (file I/O, cache, persistence). + /// + /// - Parameters: + /// - message: The message to log. + /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). + /// - file: The source file path (auto-captured). + /// - line: The source line number (auto-captured). + static func storage( + _ message: String, + category: LogCategory? = nil, + fileID: String = #fileID, + function: String = #function, + file: String = #file, + line: Int = #line + ) { + log(message, level: .storage, category: category, fileID: fileID, function: function, file: file, line: line) + } + + private static func log( + _ message: String, + level: LogLevel, + category: LogCategory?, + fileID: String, + function: String, + file: String, + line: Int + ) { + let resolvedCategory = category ?? LogCategory(fileID, isAutoGenerated: true) + logger.log(message, level: level, category: resolvedCategory, file: file, function: function, line: line) + } +} diff --git a/Sources/Scribe/Log.swift b/Sources/Scribe/Log.swift index fe90cd2..8e4e631 100644 --- a/Sources/Scribe/Log.swift +++ b/Sources/Scribe/Log.swift @@ -2,28 +2,44 @@ // Log.swift // Scribe // -// Created by Kami on 04/02/2025. +// Created by Kai Azim on 2026-01-08. // -import Foundation - -/// Static logging interface providing convenient methods for all log levels. +/// Logging interface providing convenient methods for all log levels. /// -/// Use the static methods on this enum to log messages throughout your application. -/// Each method automatically captures source location metadata (file, function, line). +/// Use the methods on this struct to log messages throughout your category. +/// This is useful if you want to use a shared logger specifically for a class, struct or enum where writing `category: +/// ...` in `Log.` would be repetitive. +/// Similar to ``Log``, Each method automatically captures source location metadata (file, function, line). /// /// ```swift -/// Log.info("User signed in", category: .init("Auth")) -/// Log.error("Failed to fetch data", category: .init("Network")) -/// Log.debug("Processing item \(item.id)") +/// let log = Log(category: .init("Auth")) +/// log.info("User signed in") +/// log.error("User session expired") +/// log.debug("New user registered") +/// ``` +/// +/// It is recommended to use this alongside the `@Loggable` macro, which automatically creates the `Log` for you. +/// You can use is as shown below: +/// +/// ```swift +/// @Loggable +/// struct TokenManager { +/// func test() { +/// log.success("New token generated") +/// } +/// } /// ``` /// -/// The `category` parameter defaults to the file ID where the log was called, -/// but you can specify custom categories for better filtering and organization. @frozen -public enum Log: Sendable { +public struct Log: Sendable { /// The shared `LogManager` instance used by all logging methods. public static var logger: LogManager { LogManager.shared } + public let category: LogCategory + + public init(category: LogCategory) { + self.category = category + } // MARK: - Debug & Development @@ -31,19 +47,11 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func debug( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .debug, category: category, fileID: fileID, function: function, file: file, line: line) + public func debug(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .debug, category: category, function: function, file: file, line: line) } /// Logs a trace message for detailed execution flow tracking. @@ -52,38 +60,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func trace( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .trace, category: category, fileID: fileID, function: function, file: file, line: line) + public func trace(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .trace, category: category, function: function, file: file, line: line) } /// Logs a print-level message as a structured replacement for `Swift.print()`. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func print( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .print, category: category, fileID: fileID, function: function, file: file, line: line) + public func print(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .print, category: category, function: function, file: file, line: line) } // MARK: - General Information @@ -92,38 +84,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func info( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .info, category: category, fileID: fileID, function: function, file: file, line: line) + public func info(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .info, category: category, function: function, file: file, line: line) } /// Logs a notice about a notable but non-critical event. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func notice( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .notice, category: category, fileID: fileID, function: function, file: file, line: line) + public func notice(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .notice, category: category, function: function, file: file, line: line) } // MARK: - Warnings & Errors @@ -132,57 +108,33 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func warn( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .warning, category: category, fileID: fileID, function: function, file: file, line: line) + public func warn(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .warning, category: category, function: function, file: file, line: line) } /// Logs an error that occurred but was handled. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func error( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .error, category: category, fileID: fileID, function: function, file: file, line: line) + public func error(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .error, category: category, function: function, file: file, line: line) } /// Logs a fatal error that may cause app termination or severe malfunction. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func fatal( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .fatal, category: category, fileID: fileID, function: function, file: file, line: line) + public func fatal(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .fatal, category: category, function: function, file: file, line: line) } // MARK: - Success & Completion @@ -191,38 +143,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func success( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .success, category: category, fileID: fileID, function: function, file: file, line: line) + public func success(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .success, category: category, function: function, file: file, line: line) } /// Logs task or operation completion. /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func done( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .done, category: category, fileID: fileID, function: function, file: file, line: line) + public func done(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .done, category: category, function: function, file: file, line: line) } // MARK: - Network Operations @@ -231,38 +167,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func network( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .network, category: category, fileID: fileID, function: function, file: file, line: line) + public func network(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .network, category: category, function: function, file: file, line: line) } /// Logs API call activity (requests, responses, errors). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func api( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .api, category: category, fileID: fileID, function: function, file: file, line: line) + public func api(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .api, category: category, function: function, file: file, line: line) } // MARK: - Security & Authentication @@ -271,38 +191,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func security( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .security, category: category, fileID: fileID, function: function, file: file, line: line) + public func security(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .security, category: category, function: function, file: file, line: line) } /// Logs authentication events (login, logout, token refresh). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func auth( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .auth, category: category, fileID: fileID, function: function, file: file, line: line) + public func auth(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .auth, category: category, function: function, file: file, line: line) } // MARK: - Performance & Analytics @@ -311,38 +215,22 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func metric( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .metric, category: category, fileID: fileID, function: function, file: file, line: line) + public func metric(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .metric, category: category, function: function, file: file, line: line) } /// Logs analytics events (user behavior, feature usage). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func analytics( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .analytics, category: category, fileID: fileID, function: function, file: file, line: line) + public func analytics(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .analytics, category: category, function: function, file: file, line: line) } // MARK: - UI & User Interaction @@ -351,38 +239,29 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func ui( + public func ui( _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, function: String = #function, file: String = #file, line: Int = #line ) { - log(message, level: .ui, category: category, fileID: fileID, function: function, file: file, line: line) + log(message, level: .ui, category: category, function: function, file: file, line: line) } /// Logs user actions (taps, gestures, input). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. + /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func user( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .user, category: category, fileID: fileID, function: function, file: file, line: line) + public func user(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .user, category: category, function: function, file: file, line: line) } // MARK: - Database & Storage @@ -391,50 +270,27 @@ public enum Log: Sendable { /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func database( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .database, category: category, fileID: fileID, function: function, file: file, line: line) + public func database(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .database, category: category, function: function, file: file, line: line) } /// Logs storage operations (file I/O, cache, persistence). /// /// - Parameters: /// - message: The message to log. - /// - category: Category for filtering. Defaults to the calling file. /// - function: The calling function (auto-captured). /// - file: The source file path (auto-captured). /// - line: The source line number (auto-captured). - public static func storage( - _ message: String, - category: LogCategory? = nil, - fileID: String = #fileID, - function: String = #function, - file: String = #file, - line: Int = #line - ) { - log(message, level: .storage, category: category, fileID: fileID, function: function, file: file, line: line) + public func storage(_ message: String, function: String = #function, file: String = #file, line: Int = #line) { + log(message, level: .storage, category: category, function: function, file: file, line: line) } - private static func log( - _ message: String, - level: LogLevel, - category: LogCategory?, - fileID: String, - function: String, - file: String, - line: Int + private func log( + _ message: String, level: LogLevel, category: LogCategory, function: String, file: String, line: Int ) { - let resolvedCategory = category ?? LogCategory(fileID, isAutoGenerated: true) - logger.log(message, level: level, category: resolvedCategory, file: file, function: function, line: line) + Log.logger.log(message, level: level, category: category, file: file, function: function, line: line) } } diff --git a/Sources/Scribe/Macros.swift b/Sources/Scribe/Macros.swift new file mode 100644 index 0000000..4ca3f7c --- /dev/null +++ b/Sources/Scribe/Macros.swift @@ -0,0 +1,121 @@ +// +// Macros.swift +// Scribe +// +// Created by Kai Azim on 2026-01-07. +// + +#if $Macros && hasAttribute(attached) + + /// A style of log to be added via the macro. + public enum LogStyle { + /// Makes the `log` a static property. + case `static` + + /// Makes the `log` an instance property. + case instance + } + + /// Defines and implements a log and a category. + /// + /// This macro helps to implement log helpers to ease the use of Scribe across your project. + /// + /// # Usage + /// + /// The `@Loggable` macro allows a type to automatically provide: + /// - a `Log` instance + /// - a `LogCategory` instance + /// specific to that type. + /// + /// Why is this useful? + /// Normally, you would need to define a `LogCategory` for each type and pass it into every log statement. + /// It's easy to forget to create or use the correct category, which can lead to inconsistent logging. + /// `@Loggable` solves this by automatically generating the appropriate category for you, + /// ensuring that every log statement is correctly categorized. + /// + /// Example before using `@Loggable`: + /// ```swift + /// final class Updater { + /// func fetchLatestInfo() { + /// Log.info("Checking for updates...", category: .updater) + /// // ... + /// Log.info("Finished for updates!", category: .updater) + /// } + /// + /// func installUpdate() { + /// Log.info("Installing update...", category: .updater) + /// // ... + /// Log.info("Installed update, ready to relaunch!", category: .updater) + /// } + /// } + /// + /// extension LogCategory { + /// static let updater = LogCategory("Updater") + /// } + /// ``` + /// + /// With `@Loggable`: + /// ```swift + /// @Loggable + /// final class Updater { + /// func fetchLatestInfo() { + /// log.info("Checking for updates...") + /// // ... + /// log.info("Finished for updates!") + /// } + /// + /// func installUpdate() { + /// log.info("Installing update...") + /// // ... + /// log.info("Installed update, ready to relaunch!") + /// } + /// } + /// ``` + /// + /// Using this macro gives you a cleaner, safer workflow: + /// - No need to manually define a `LogCategory` + /// - You won’t forget to add the correct category + /// - Every log statement automatically uses the right category for the type + /// + /// # Log Styles + /// + /// This macro attaches two or three properties to the given type depending on the used log style: + /// + /// **Static Log Style** + /// + /// Usage: `@Loggable(style: .static)` + /// + /// - A static property with the name `log` is exposed, of type `Log`. + /// - A static property with the name `logCategory` is exposed, of type `LogCategory`. This category is used in the + /// Log to help categorize this type. + /// + /// ## Instance Log Style + /// + /// Usage: `@Loggable(style: .instance)` or simply `@Loggable` + /// + /// - An instance property with the name `log` is exposed, of type `Log`. This is actually just a computed variable + /// for the static property defined below: + /// - A private, static property with the name `_log` is exposed, of type `Log`. Expect this to work in the same way + /// that the static log style defines `log`, just that it is private. This is used to store a single, shared + /// instance across all instances of this type. + /// - A static property with the name `logCategory` is exposed, of type `LogCategory`. This category is used in the + /// Log to help categorize this type. + /// + /// # Naming + /// + /// By default, the `@Loggable` macro uses the type’s name as the log category. + /// If you’d like to override this, you can provide a custom name as the macro’s first argument. + /// + /// For example, applying `@Loggable("Network")` to a type named `NetworkManager` + /// will create a `LogCategory` named `Network` instead of `NetworkManager`. + /// + @attached(member, names: named(log), named(_log), named(logCategory)) + public macro Loggable( + _ name: StaticString? = nil, + category: LogCategory? = nil, + style: LogStyle = .instance + ) = #externalMacro( + module: "ScribeMacros", + type: "LoggableMacro" + ) +#endif diff --git a/Sources/ScribeMacros/LoggableMacro.swift b/Sources/ScribeMacros/LoggableMacro.swift new file mode 100644 index 0000000..69286fd --- /dev/null +++ b/Sources/ScribeMacros/LoggableMacro.swift @@ -0,0 +1,262 @@ +// +// LoggableMacro.swift +// Scribe +// +// Created by Kai Azim on 2026-01-07. +// + +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +// MARK: - MacrosPlugin + +@main +struct MacrosPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + LoggableMacro.self + ] +} + +// MARK: - LoggableMacro + +struct LoggableMacro: MemberMacro { + static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclSyntaxProtocol, + conformingTo protocolType: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard let typeName = getTypeName(declaration: declaration) else { + return [] + } + + let accessLevel = getAccessLevel(declaration: declaration) + let accessPrefix = accessLevel.isEmpty ? "" : "\(accessLevel) " + var options = parseLogOptions(from: node) + + // If both a custom name and category are passed in, they conflict. + if options.name != nil, options.categoryExpr != nil { + context.diagnose( + Diagnostic( + node: Syntax(node), + message: LoggableArgumentConflictDiagnostic() + ) + ) + return [] + } + + // If no explicit category is provided, fall back to type name + if options.categoryExpr == nil, options.name == nil { + options.name = typeName + } + + var members: [DeclSyntax] = [] + + if let categoryExpr = options.categoryExpr { + // If the user provided a category, then make computed static var + members.append( + DeclSyntax( + """ + \(raw: accessPrefix)static var logCategory: LogCategory { + \(categoryExpr) + } + """ + ) + ) + } else if let name = options.name { + // Otherwise, create a new category + members.append( + DeclSyntax( + """ + \(raw: accessPrefix)static let logCategory: LogCategory = LogCategory("\(raw: name)") + """ + ) + ) + } + + switch options.style { + case .static: + members.append( + DeclSyntax( + """ + \(raw: accessPrefix)static let log = Log(category: logCategory) + """ + ) + ) + case .instance: + members.append( + DeclSyntax( + """ + private static let _log = Log(category: \(raw: typeName).logCategory) + """ + ) + ) + + members.append( + DeclSyntax( + """ + \(raw: accessPrefix)var log: Log { \(raw: typeName)._log } + """ + ) + ) + } + + return members + } + + /// Extracts the name of the type from the declaration. + /// + /// This is used to: + /// - Generate the default `LogCategory` name when no custom name is provided. + /// - Reference the type explicitly in the generated `_log` property (e.g., `TypeName.logCategory`). + /// This is because using `Self.<>` can lead to covariant self errors. + /// + /// Returns `nil` if the declaration is not a supported type (enum, struct, or class). + private static func getTypeName(declaration: some DeclSyntaxProtocol) -> String? { + if let decl = declaration.as(EnumDeclSyntax.self) { + decl.name.text + } else if let decl = declaration.as(StructDeclSyntax.self) { + decl.name.text + } else if let decl = declaration.as(ClassDeclSyntax.self) { + decl.name.text + } else { + nil + } + } + + /// Extracts the access level modifier from the declaration. + /// + /// This ensures the generated properties (`logCategory`, `log`) match the access level of the type they're + /// attached to, rather than being hardcoded to `public`. + /// + /// Returns an empty string if no explicit access level is found. + private static func getAccessLevel(declaration: some DeclSyntaxProtocol) -> String { + let modifiers: DeclModifierListSyntax? = if let decl = declaration.as(EnumDeclSyntax.self) { + decl.modifiers + } else if let decl = declaration.as(StructDeclSyntax.self) { + decl.modifiers + } else if let decl = declaration.as(ClassDeclSyntax.self) { + decl.modifiers + } else { + nil + } + + guard let modifiers else { return "" } + + for modifier in modifiers { + switch modifier.name.text { + case "public", + "package", + "internal", + "fileprivate", + "private": + return modifier.name.text + default: + continue + } + } + + return "" + } + + /// Parses the logger name and style to be used with this macro. + /// + /// By default: + /// - `categoryExpr` is `nil` + /// - `name` is `nil` + /// - `style` is `.instance` + /// + /// Supported forms: + /// - `@Loggable` + /// - `@Loggable("network")` + /// - `@Loggable(type: .static)` + /// - `@Loggable(category: .network)` + /// - `@Loggable("network", type: .static)` + /// - `@Loggable(category: .network, type: .instance)` + private static func parseLogOptions(from node: AttributeSyntax) -> ParsedLogOptions { + var name: String? = nil + var categoryExpr: ExprSyntax? = nil + var style: LogStyle = .instance + + guard let arguments = node.arguments?.as(LabeledExprListSyntax.self) else { + return ParsedLogOptions(categoryExpr: nil, name: nil, style: style) + } + + for argument in arguments { + let expr = argument.expression + + // category: + if argument.label?.text == "category" { + categoryExpr = expr + continue + } + + // String literal → name + if let stringLiteral = expr.as(StringLiteralExprSyntax.self) { + name = stringLiteral.segments + .compactMap { $0.as(StringSegmentSyntax.self)?.content.text } + .joined() + continue + } + + // Member access → style (.static / .instance) + if let memberAccess = expr.as(MemberAccessExprSyntax.self) { + switch memberAccess.declName.baseName.text { + case "static": + style = .static + case "instance": + style = .instance + default: + break + } + } + } + + return ParsedLogOptions( + categoryExpr: categoryExpr, + name: name, + style: style + ) + } + + /// A style of log to be added via the macro. Make sure this matches the declarations over in the Scribe target! + private enum LogStyle { + /// Makes the `log` a static property. + case `static` + + /// Makes the `log` an instance property. + case instance + } + + /// A struct to expose a type-safe version of the macro's arguments. + private struct ParsedLogOptions { + /// Explicit category expression, e.g. `.network`. + var categoryExpr: ExprSyntax? + + /// The name of the LogCategory to generate. If the `name` and `categoryExpr` are `nil`, then the type name is + /// used. + var name: String? + + /// The style of log to be added via the macro. + let style: LogStyle + } +} + +// MARK: - LoggableArgumentConflictDiagnostic + +private struct LoggableArgumentConflictDiagnostic: DiagnosticMessage { + var message: String { + "`@Loggable` cannot specify both a custom name and a category. Use only one." + } + + var diagnosticID: MessageID { + MessageID(domain: "LoggableMacro", id: "nameAndCategoryConflict") + } + + var severity: DiagnosticSeverity { + .error + } +} diff --git a/Tests/Scribe/LogCategory+Extensions.swift b/Tests/Scribe/LogCategory+Extensions.swift new file mode 100644 index 0000000..a8a2760 --- /dev/null +++ b/Tests/Scribe/LogCategory+Extensions.swift @@ -0,0 +1,14 @@ +@testable @_spi(Internals) import Scribe + +extension LogCategory { + static let test = LogCategory("TestCategory") + + static let testAllowed = LogCategory("AllowedCategory") + static let testBlocked = LogCategory("BlockedCategory") + + static let evictOneAuto = LogCategory("Auto/EvictOne.swift", isAutoGenerated: true) + static let evictTwoAuto = LogCategory("Auto/EvictTwo.swift", isAutoGenerated: true) + static let evictThreeAuto = LogCategory("Auto/EvictThree.swift", isAutoGenerated: true) + + static let passedInCategory = LogCategory("PassedInCategory") +} diff --git a/Tests/Scribe/LoggableTests.swift b/Tests/Scribe/LoggableTests.swift new file mode 100644 index 0000000..540acd2 --- /dev/null +++ b/Tests/Scribe/LoggableTests.swift @@ -0,0 +1,100 @@ +import XCTest +@testable import Scribe + +// MARK: - LoggableClass + +@Loggable +final class LoggableClass { + func doWork() { log.info("LoggableClass working") } +} + +// MARK: - LoggableStruct + +@Loggable +struct LoggableStruct { + func doWork() { log.info("LoggableStruct working") } +} + +// MARK: - LoggableEnum + +@Loggable +enum LoggableEnum { + case one + func doWork() { log.info("LoggableEnum working") } +} + +// MARK: - LoggableCustomName + +@Loggable("CustomName") +final class LoggableCustomName { + func doWork() { log.info("LoggableCustomName working") } +} + +// MARK: - LoggablePassedCategory + +@Loggable(category: .passedInCategory) +final class LoggablePassedCategory { + func doWork() { log.info("LoggablePassedCategory working") } +} + +// MARK: - LoggableClassStatic + +@Loggable(style: .static) +final class LoggableClassStatic { + static func doWork() { log.info("LoggableClassStatic working") } +} + +// MARK: - LoggableMacroTests + +final class LoggableMacroTests: XCTestCase { + override func setUp() { + super.setUp() + Log.logger.setMinimumLevel(.debug) + Log.logger.setConfiguration(.default) + Log.logger.removeAllSinks() + } + + func testLoggableClass() { + XCTAssertEqual(LoggableClass.logCategory.name, "LoggableClass") + XCTAssertEqual(LoggableClass().log.category.name, "LoggableClass") + } + + func testLoggableStruct() { + XCTAssertEqual(LoggableStruct.logCategory.name, "LoggableStruct") + XCTAssertEqual(LoggableStruct().log.category.name, "LoggableStruct") + } + + func testLoggableEnum() { + XCTAssertEqual(LoggableEnum.logCategory.name, "LoggableEnum") + XCTAssertEqual(LoggableEnum.one.log.category.name, "LoggableEnum") + } + + func testLoggableStaticStyle() { + XCTAssertEqual(LoggableClassStatic.logCategory.name, "LoggableClassStatic") + XCTAssertEqual(LoggableClassStatic.log.category.name, "LoggableClassStatic") + } + + func testLoggableCustomName() { + XCTAssertEqual(LoggableCustomName.logCategory.name, "CustomName") + XCTAssertEqual(LoggableCustomName().log.category.name, "CustomName") + } + + func testLoggablePassedCategory() { + XCTAssertEqual(LoggablePassedCategory.logCategory.name, "PassedInCategory") + XCTAssertEqual(LoggablePassedCategory().log.category.name, "PassedInCategory") + } + + func testLoggableActuallyLogs() { + let expectation = XCTestExpectation(description: "Log received") + + Log.logger.addSink { message in + if message.contains("LoggableClass working"), message.contains("[LoggableClass]") { + expectation.fulfill() + } + } + + LoggableClass().doWork() + + wait(for: [expectation], timeout: 2.0) + } +} diff --git a/Tests/Scribe/ScribeTests.swift b/Tests/Scribe/ScribeTests.swift index 51f7a28..efdf5a1 100644 --- a/Tests/Scribe/ScribeTests.swift +++ b/Tests/Scribe/ScribeTests.swift @@ -1,9 +1,6 @@ import XCTest - @testable @_spi(Internals) import Scribe -// MARK: - ScribeTests - final class ScribeTests: XCTestCase { override func setUp() { super.setUp() @@ -363,14 +360,3 @@ final class ScribeTests: XCTestCase { Log.logger.removeSink(sinkID) } } - -extension LogCategory { - static let test = LogCategory("TestCategory") - - static let testAllowed = LogCategory("AllowedCategory") - static let testBlocked = LogCategory("BlockedCategory") - - static let evictOneAuto = LogCategory("Auto/EvictOne.swift", isAutoGenerated: true) - static let evictTwoAuto = LogCategory("Auto/EvictTwo.swift", isAutoGenerated: true) - static let evictThreeAuto = LogCategory("Auto/EvictThree.swift", isAutoGenerated: true) -}