diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..47925e5 --- /dev/null +++ b/License.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2023 François Lamboley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift index 876dc58..cdd934d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.1 import PackageDescription @@ -13,7 +13,7 @@ let package = Package( targets: [ .target(name: "OSLogLogger", dependencies: [ .product(name: "Logging", package: "swift-log"), - ], path: "Sources"), + ], path: "Sources", exclude: ["OSLogLogger+WithSendable.swift"]), .testTarget(name: "OSLogLoggerTests", dependencies: ["OSLogLogger"], path: "Tests"), ] ) diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift new file mode 100644 index 0000000..b4ec772 --- /dev/null +++ b/Package@swift-5.8.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.8 +import PackageDescription + + +//let swiftSettings: [SwiftSetting] = [] +let swiftSettings: [SwiftSetting] = [.enableExperimentalFeature("StrictConcurrency")] + +let package = Package( + name: "oslog-logger", + products: [ + .library(name: "OSLogLogger", targets: ["OSLogLogger"]) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + ], + targets: [ + .target(name: "OSLogLogger", dependencies: [ + .product(name: "Logging", package: "swift-log"), + ], path: "Sources", exclude: ["OSLogLogger+NoSendable.swift"], swiftSettings: swiftSettings), + .testTarget(name: "OSLogLoggerTests", dependencies: ["OSLogLogger"], path: "Tests", swiftSettings: swiftSettings), + ] +) diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..f735ac6 --- /dev/null +++ b/Readme.md @@ -0,0 +1 @@ +# 🍎 OSLogLogger diff --git a/Sources/DummySendable.swift b/Sources/DummySendable.swift new file mode 100644 index 0000000..dd28069 --- /dev/null +++ b/Sources/DummySendable.swift @@ -0,0 +1,17 @@ +import Foundation +import os + + +/* Sendable exists from Swift 5.5, however OSLog is only Sendable starting from Xcode with at least Swift 5.8. + * That being said, the Sendability issues regarding OSLog not being Sendable only trigger errors when compiling with Swift 5.5 exactly, + * so we cheat and make a dummy Sendable protocol for Swift 5.5 too. */ +#if swift(>=5.6) +public protocol GHALogger_Sendable : Sendable {} +#else +public protocol GHALogger_Sendable {} +#endif + +#if swift(>=5.3) +@available(macOS 11.0, tvOS 14.0, iOS 14.0, watchOS 7.0, *) +extension Logger : GHALogger_Sendable {} +#endif diff --git a/Sources/OSLogLogger+NoSendable.swift b/Sources/OSLogLogger+NoSendable.swift new file mode 100644 index 0000000..06de2b9 --- /dev/null +++ b/Sources/OSLogLogger+NoSendable.swift @@ -0,0 +1,31 @@ +import Foundation + +import Logging + + + +@available(macOS 10.12, tvOS 10.0, iOS 10.0, watchOS 3.0, *) +extension OSLogLogger { + + /** + Convenience init that splits the label in a subsystem and a category. + + The format of the label should be as follow: "subsystem:category". + The subsystem _should_ be a reverse-DNS identifier (as per Apple doc). + Example: "`com.xcode-actions.oslog-logger:LogHandler`". + + If there is no colon in the given label + we set the category to “``” (it cannot be `nil`, surprisingly, and we decided against the empty String to be able to still filter this category) + and we use the whole label for the subsystem. + + It is _not_ possible to have a subsystem containing a colon using this initializer. */ + public init(label: String, metadataProvider: Logging.Logger.MetadataProvider? = LoggingSystem.metadataProvider) { + let split = label.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + let subsystem = split[0] /* Cannot not exists as we do not omit empty subsequences in the split. */ + let categoryCollection = split.dropFirst() + assert(categoryCollection.count <= 1) + + self.init(subsystem: String(subsystem), category: categoryCollection.first.flatMap(String.init) ?? "", metadataProvider: metadataProvider) + } + +} diff --git a/Sources/OSLogLogger+WithSendable.swift b/Sources/OSLogLogger+WithSendable.swift new file mode 100644 index 0000000..63e2882 --- /dev/null +++ b/Sources/OSLogLogger+WithSendable.swift @@ -0,0 +1,35 @@ +import Foundation + +import Logging + + + +/* The @Sendable attribute is only available starting at Swift 5.5. + * We make these methods only available starting at Swift 5.8 for our convenience (avoids creating another Package@swift-... file) + * and because for Swift <5.8 the non-@Sendable variants of the methods are available. */ +@available(macOS 10.12, tvOS 10.0, iOS 10.0, watchOS 3.0, *) +extension OSLogLogger { + + /** + Convenience init that splits the label in a subsystem and a category. + + The format of the label should be as follow: "subsystem:category". + The subsystem _should_ be a reverse-DNS identifier (as per Apple doc). + Example: "`com.xcode-actions.oslog-logger:LogHandler`". + + If there is no colon in the given label + we set the category to “``” (it cannot be `nil`, surprisingly, and we decided against the empty String to be able to still filter this category) + and we use the whole label for the subsystem. + + It is _not_ possible to have a subsystem containing a colon using this initializer. */ + @Sendable + public init(label: String, metadataProvider: Logging.Logger.MetadataProvider? = LoggingSystem.metadataProvider) { + let split = label.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + let subsystem = split[0] /* Cannot not exists as we do not omit empty subsequences in the split. */ + let categoryCollection = split.dropFirst() + assert(categoryCollection.count <= 1) + + self.init(subsystem: String(subsystem), category: categoryCollection.first.flatMap(String.init) ?? "", metadataProvider: metadataProvider) + } + +} diff --git a/Sources/OSLogLogger.swift b/Sources/OSLogLogger.swift index cfbe55a..5f87170 100644 --- a/Sources/OSLogLogger.swift +++ b/Sources/OSLogLogger.swift @@ -5,6 +5,7 @@ import Logging +@available(macOS 10.12, tvOS 10.0, iOS 10.0, watchOS 3.0, *) public struct OSLogLogger : LogHandler { public static let pubMetaPrefix = "pub." @@ -16,31 +17,14 @@ public struct OSLogLogger : LogHandler { } public var metadataProvider: Logging.Logger.MetadataProvider? - /** - Convenience init that splits the label in a subsystem and a category. - - The format of the lable should be as follow: "subsystem:category". - The subsystem _should_ be a reverse-DNS identifier (as per Apple doc). - Example: "`com.xcode-actions.oslog-logger:LogHandler`". - - If there is no colon in the given label - we set the category to “``” (it cannot be `nil`, suprisingly, and we decided against the empty String to be able to still filter this category) - and we use the whole label for the subsystem. - - It is _not_ possible to have a subsystem containing a colon using this initializer. */ - public init(label: String, metadataProvider: Logging.Logger.MetadataProvider? = LoggingSystem.metadataProvider) { - let split = label.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) - let subsystem = split[0] /* Cannot not exists as we do not omit empty subsequences in the split. */ - let categoryCollection = split.dropFirst() - assert(categoryCollection.count <= 1) - - self.init(subsystem: String(subsystem), category: categoryCollection.first.flatMap(String.init) ?? "", metadataProvider: metadataProvider) - } - public init(subsystem: String, category: String, metadataProvider: Logging.Logger.MetadataProvider? = LoggingSystem.metadataProvider) { self.metadataProvider = metadataProvider if #available(macOS 11, tvOS 14, iOS 14, watchOS 7, *) { +#if swift(>=5.3) self.l = .logger(os.Logger(subsystem: subsystem, category: category)) +#else + self.l = .oslog(.init(subsystem: subsystem, category: category)) +#endif } else { self.l = .oslog(.init(subsystem: subsystem, category: category)) } @@ -57,17 +41,23 @@ public struct OSLogLogger : LogHandler { public init(oslog: OSLog, metadataProvider: Logging.Logger.MetadataProvider? = LoggingSystem.metadataProvider) { self.metadataProvider = metadataProvider if #available(macOS 11, tvOS 14, iOS 14, watchOS 7, *) { +#if swift(>=5.3) self.l = .logger(os.Logger(oslog)) +#else + self.l = .oslog(oslog) +#endif } else { self.l = .oslog(oslog) } } +#if swift(>=5.3) @available(macOS 11, tvOS 14, iOS 14, watchOS 7, *) public init(logger: os.Logger, metadataProvider: Logging.Logger.MetadataProvider? = LoggingSystem.metadataProvider) { self.metadataProvider = metadataProvider self.l = .logger(logger) } +#endif public subscript(metadataKey metadataKey: String) -> Logging.Logger.Metadata.Value? { get {metadata[metadataKey]} @@ -85,6 +75,7 @@ public struct OSLogLogger : LogHandler { else {effectiveFlatMetadata = flatMetadataCache} if #available(macOS 11, tvOS 14, iOS 14, watchOS 7, *) { +#if swift(>=5.3) /* If we could use os.Logger directly. * Note these calls probably do more or less what the os_log call above does… */ switch level { @@ -138,6 +129,14 @@ public struct OSLogLogger : LogHandler { case (false, false): l.logger.critical("\(message, privacy: .public)\n ▷ \(effectiveFlatMetadata.public .joined(separator: "\n ▷ "), privacy: .public)\n ▷ \(effectiveFlatMetadata.private.joined(separator: "\n ▷ "), privacy: .private)") } } +#else + switch (effectiveFlatMetadata.public.isEmpty, effectiveFlatMetadata.private.isEmpty) { + case ( true, true): os_log("%{public}@", log: l.oslog, type: Self.logLevelToLogType(level), "\(message)") + case (false, true): os_log("%{public}@\n ▷ %{public}@", log: l.oslog, type: Self.logLevelToLogType(level), "\(message)", effectiveFlatMetadata.public .joined(separator: "\n ▷ ")) + case ( true, false): os_log("%{public}@\n ▷ %{private}@", log: l.oslog, type: Self.logLevelToLogType(level), "\(message)", effectiveFlatMetadata.private.joined(separator: "\n ▷ ")) + case (false, false): os_log("%{public}@\n ▷ %{public}@\n ▷ %{private}@", log: l.oslog, type: Self.logLevelToLogType(level), "\(message)", effectiveFlatMetadata.public .joined(separator: "\n ▷ "), effectiveFlatMetadata.private.joined(separator: "\n ▷ ")) + } +#endif } else { switch (effectiveFlatMetadata.public.isEmpty, effectiveFlatMetadata.private.isEmpty) { @@ -174,13 +173,14 @@ public struct OSLogLogger : LogHandler { /* Adapted from CLTLogger. */ +@available(macOS 10.12, tvOS 10.0, iOS 10.0, watchOS 3.0, *) private extension OSLogLogger { /** Merge the logger’s metadata, the provider’s metadata and the given explicit metadata and return the new metadata. If the provider’s metadata and the explicit metadata are `nil`, returns `nil` to signify the current `flatMetadataCache` can be used. */ func mergedMetadata(with explicit: Logging.Logger.Metadata?) -> Logging.Logger.Metadata? { - var metadata = metadata + var metadata = self.metadata let provided = metadataProvider?.get() ?? [:] guard !provided.isEmpty || !((explicit ?? [:]).isEmpty) else { @@ -226,11 +226,11 @@ private extension OSLogLogger { func prettyMetadataValue(_ v: Logging.Logger.MetadataValue) -> String { /* We return basically v.description, but dictionary keys are sorted. */ - return switch v { - case .string(let str): str.processForLogging(escapingMode: .escapeScalars(asASCII: true, octothorpLevel: nil, showQuotes: true), newLineProcessing: .escape).string - case .array(let array): #"["# + array.map{ prettyMetadataValue($0) }.joined(separator: ", ") + #"]"# - case .dictionary(let dict): #"["# + flatMetadataArray(dict).joined(separator: ", ") + #"]"# - case .stringConvertible(let c): prettyMetadataValue(.string(c.description)) + switch v { + case .string(let str): return str.processForLogging(escapingMode: .escapeScalars(asASCII: true, octothorpLevel: nil, showQuotes: true), newLineProcessing: .escape).string + case .array(let array): return #"["# + array.map{ prettyMetadataValue($0) }.joined(separator: ", ") + #"]"# + case .dictionary(let dict): return #"["# + flatMetadataArray(dict).joined(separator: ", ") + #"]"# + case .stringConvertible(let c): return prettyMetadataValue(.string(c.description)) } } diff --git a/Sources/String+Utils.swift b/Sources/String+Utils.swift index 89dcc31..fc8b1cb 100644 --- a/Sources/String+Utils.swift +++ b/Sources/String+Utils.swift @@ -38,12 +38,20 @@ internal extension String { case .escapeScalars(_, let octothorpLevel?, _, true): let octothorps = String(repeating: "#", count: Int(octothorpLevel)) - return (octothorpLevel, octothorps + #"""#, #"""# + octothorps) +//#if swift(>=5.4) +// return (octothorpLevel, octothorps + #"""#, #"""# + octothorps) +//#else + return (octothorpLevel, octothorps + "\"", "\"" + octothorps) +//#endif case .escapeScalars(_, nil, _, let showQuotes): /* We must determine the octothorp level. */ var level = UInt(0) - var (sepOpen, sepClose) = (#"""#, #"""#) +//#if swift(>=5.4) +// var (sepOpen, sepClose) = (#"""#, #"""#) +//#else + var (sepOpen, sepClose) = ("\"", "\"") +//#endif while str.contains(sepClose) { level += 1 sepOpen = "#" + sepOpen @@ -65,7 +73,7 @@ internal extension String { var hasProcessedNewLines = false var specialCharState: (UnicodeScalar, Int)? = nil /* First element is the special char, the other is the number of octothorps found. */ - let ascii = unicodeScalars.lazy.map{ scalar in + let ascii = unicodeScalars.lazy.map{ scalar -> String in /* Let’s build the previous escape if needed. */ let prefix: String if scalar == "#" { @@ -75,7 +83,11 @@ internal extension String { if curSpecial.1 == octothorpLevel - 1 { /* We have now reached the number of octothorp needed to build an actual “special char” (closing quote, backslash, etc.); we must escape it. */ specialCharState = nil - return #"\"# + octothorps + String(curSpecial.0) + octothorps +//#if swift(>=5.4) +// return #"\"# + octothorps + String(curSpecial.0) + octothorps +//#else + return "\\" + octothorps + String(curSpecial.0) + octothorps +//#endif } specialCharState = (curSpecial.0, curSpecial.1 + 1) return "" @@ -87,7 +99,8 @@ internal extension String { prefix = "" } - if Self.newLines.contains(scalar) { + let isNewLine = Self.newLines.contains(scalar) + if isNewLine { hasProcessedNewLines = true switch newLineProcessing { case .none: return prefix + String(scalar) @@ -111,14 +124,24 @@ internal extension String { specialCharState = (scalar, 0) return prefix + "" } - let escaped = scalar.escaped(asASCII: asASCII) - return prefix + (octothorpLevel == 0 ? escaped : escaped.replacingOccurrences(of: #"\"#, with: #"\"# + octothorps, options: .literal)) + let escaped = scalar.escaped(asASCII: asASCII || isNewLine) +//#if swift(>=5.4) +// return prefix + (octothorpLevel == 0 ? escaped : escaped.replacingOccurrences(of: #"\"#, with: #"\"# + octothorps, options: .literal)) +//#else + return prefix + (octothorpLevel == 0 ? escaped : escaped.replacingOccurrences(of: "\\", with: "\\" + octothorps, options: .literal)) +//#endif } } - return (sepOpen + ascii.joined(separator: "") + (specialCharState.flatMap{ String($0.0) + String(repeating: "#", count: $0.1) } ?? "") + sepClose, hasProcessedNewLines) + let asciiJoined = ascii.joined(separator: "") + let specialCharStateMapped = (specialCharState.flatMap{ String($0.0) + String(repeating: "#", count: $0.1) } ?? "") + return (sepOpen + asciiJoined + specialCharStateMapped + sepClose, hasProcessedNewLines) } private static let newLines = CharacterSet.newlines - private static let specialChars = CharacterSet(charactersIn: #""\"#) - +//#if swift(>=5.4) +// private static let specialChars = CharacterSet(charactersIn: #""\"#) +//#else + private static let specialChars = CharacterSet(charactersIn: "\"\\") +//#endif + } diff --git a/Sources/UnderlyingLogger.swift b/Sources/UnderlyingLogger.swift index c5779d2..4127509 100644 --- a/Sources/UnderlyingLogger.swift +++ b/Sources/UnderlyingLogger.swift @@ -7,12 +7,12 @@ import os * On macOS 11+, tvOS 14+, etc. we use os.Logger. * On lower platforms we use OSLog. * - * Why not use OSLog anywhere? + * Why not use OSLog everywhere? * Because it is broken (at least on macOS 14/iOS 17) and the subsystem and category are not properly set. */ -internal enum UnderlyingLogger { +internal enum UnderlyingLogger : GHALogger_Sendable { case oslog(OSLog) - case logger(Any) + case logger(GHALogger_Sendable) var oslog: OSLog! { switch self { @@ -21,6 +21,7 @@ internal enum UnderlyingLogger { } } +#if swift(>=5.3) @available(macOS 11, tvOS 14, iOS 14, watchOS 7, *) var logger: Logger! { switch self { @@ -28,5 +29,6 @@ internal enum UnderlyingLogger { case .logger(let r): return (r as! Logger) } } +#endif } diff --git a/Tests/OSLogLoggerTests.swift b/Tests/OSLogLoggerTests.swift index e0de983..2563c8b 100644 --- a/Tests/OSLogLoggerTests.swift +++ b/Tests/OSLogLoggerTests.swift @@ -7,6 +7,7 @@ import Logging +@available(macOS 10.12, tvOS 10.0, iOS 10.0, watchOS 3.0, *) final class OSLogLoggerTests : XCTestCase { override class func setUp() {