Skip to content
Draft
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
21 changes: 21 additions & 0 deletions License.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright © 2023 François Lamboley <[email protected]>

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.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.1
import PackageDescription


Expand All @@ -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"),
]
)
22 changes: 22 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
@@ -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),
]
)
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# 🍎 OSLogLogger
17 changes: 17 additions & 0 deletions Sources/DummySendable.swift
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions Sources/OSLogLogger+NoSendable.swift
Original file line number Diff line number Diff line change
@@ -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 “`<none>`” (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) ?? "<none>", metadataProvider: metadataProvider)
}

}
35 changes: 35 additions & 0 deletions Sources/OSLogLogger+WithSendable.swift
Original file line number Diff line number Diff line change
@@ -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 “`<none>`” (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) ?? "<none>", metadataProvider: metadataProvider)
}

}
54 changes: 27 additions & 27 deletions Sources/OSLogLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -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 “`<none>`” (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) ?? "<none>", 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))
}
Expand All @@ -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]}
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
}

Expand Down
43 changes: 33 additions & 10 deletions Sources/String+Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 == "#" {
Expand All @@ -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 ""
Expand All @@ -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)
Expand All @@ -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

}
8 changes: 5 additions & 3 deletions Sources/UnderlyingLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -21,12 +21,14 @@ internal enum UnderlyingLogger {
}
}

#if swift(>=5.3)
@available(macOS 11, tvOS 14, iOS 14, watchOS 7, *)
var logger: Logger! {
switch self {
case .oslog: return nil
case .logger(let r): return (r as! Logger)
}
}
#endif

}
1 change: 1 addition & 0 deletions Tests/OSLogLoggerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down