Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Change Log
All significant changes to this project will be documented in this file.

## [5.1.0](https://github.com/tonystone/tracelog/tree/5.1.0)

#### Added
- Added the UnifiedLogWriter to write to the Apple Unified Logging system on Apple platforms.

## [5.0.1](https://github.com/tonystone/tracelog/tree/5.0.1)

#### Fixed
Expand Down
42 changes: 34 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// swift-tools-version:5.0
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
///
/// Package.swift
///
Expand All @@ -22,17 +23,42 @@ import PackageDescription

let package = Package(
name: "TraceLog",
platforms: [.iOS(.v9), .macOS(.v10_13), .tvOS(.v9), .watchOS(.v2)],
products: [
.library(
name: "TraceLog",
targets: ["TraceLog"]
),
.library(
name: "TraceLogObjC",
targets: ["TraceLogObjC"]
)],
targets: [
/// Module targets
.target(name: "TraceLog", dependencies: [], path: "Sources/TraceLog"),
.target(name: "TraceLogObjC", dependencies: ["TraceLog"], path: "Sources/TraceLogObjC"),
.target(name: "TraceLog",
dependencies: [],
path: "Sources/TraceLog"),
.target(name: "TraceLogObjC",
dependencies: ["TraceLog"],
path: "Sources/TraceLogObjC",
publicHeadersPath: "include",
cSettings: [
.headerSearchPath("../.."),
]),

/// Tests
.testTarget(name: "TraceLogTests", dependencies: ["TraceLog"], path: "Tests/TraceLogTests"),
.testTarget(name: "TraceLogObjCTests", dependencies: ["TraceLogObjC"], path: "Tests/TraceLogObjCTests")
.testTarget(name: "TraceLogTests",
dependencies: ["TraceLog"],
path: "Tests/TraceLogTests",
exclude: ["Internal/Utilities/Streams/FileOutputStreamError+PosixTests.swift.gyb",
"Internal/Utilities/Streams/OutputStreamError+PosixTests.swift.gyb",
"Writers & Formatters/Textformat+EncodingTests.swift.gyb",
"Writers & Formatters/TextFormat+InternationalLanguagesTests.swift.gyb",
"Writers & Formatters/FileStrategyManager+FailureReasonTests.swift.gyb"]),
.testTarget(name: "TraceLogObjCTests",
dependencies: ["TraceLogObjC"],
path: "Tests/TraceLogObjCTests",
exclude: [])
],
swiftLanguageVersions: [.version("5")]
)

/// Main products section
package.products.append(.library(name: "TraceLog", type: .dynamic, targets: ["TraceLog", "TraceLogObjC"]))
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
* Built-in (`OutputStreamWriter`s)
* **Stdout (ConsoleWriter)** - A simple standard out (stdout) writer for logging to the console or terminal.
* **File (FileWriter)** - A file writer which writes log output to files on local disk managing rotation and archive of files as needed.
* **Apple Unified Logging (UnifiedLogWriter)** - On Apple platforms the AdaptiveWriter writes to the Unified Logging System
* External
* **Apple Unified Logging (AdaptiveWriter)** - On Apple platforms the AdaptiveWriter writes to the Unified Logging System (see [https://github.com/tonystone/tracelog-adaptive-writer](https://github.com/tonystone/tracelog-adaptive-writer)).
* **Linux systemd Journal (AdaptiveWriter)** - On Linux platforms the AdaptiveWriter writes to the systemd journal (see [https://github.com/tonystone/tracelog-adaptive-writer](https://github.com/tonystone/tracelog-adaptive-writer))
Expand Down
106 changes: 106 additions & 0 deletions Sources/TraceLog/Writers/UnifiedLogWriter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@

///
/// UnifiedLogWriter.swift
///
/// Copyright 2022 Tony Stone
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Created by Tony Stone on 4/8/22.
///
///
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)

import Foundation
import Swift
import os.log

public struct Platform {
///
/// The LogLevel type for all Platforms.
///
public typealias LogLevel = Int32
}

///
/// Apple Unified Logging System log writer for TraceLog.
///
/// Implementation of a TraceLog `Writer` to write to Apple's Unified Logging System.
///
/// - SeeAlso: https://developer.apple.com/documentation/os/logging
///
@available(iOS 10.0, macOS 10.12, watchOS 3.0, tvOS 10.0, *)
public class UnifiedLogWriter: Writer {

///
/// The default LogLevel Conversion Table.
///
public static let defaultLogLevelConversion: [TraceLog.LogLevel: Platform.LogLevel] = [
.error: Platform.LogLevel(OSLogType.error.rawValue),
.warning: Platform.LogLevel(OSLogType.default.rawValue),
.info: Platform.LogLevel(OSLogType.default.rawValue),
.trace1: Platform.LogLevel(OSLogType.debug.rawValue),
.trace2: Platform.LogLevel(OSLogType.debug.rawValue),
.trace3: Platform.LogLevel(OSLogType.debug.rawValue),
.trace4: Platform.LogLevel(OSLogType.debug.rawValue)
]

///
/// Custom subsystem passed to os_log
///
internal let subsystem: String

///
/// A dictionary keyed by TraceLog LogLevels with the value to convert to the os_log level.
///
private let logLevelConversion: [LogLevel: Platform.LogLevel]

///
/// Initializes an UnifiedLoggingWriter.
///
/// - Parameters:
/// - sybsystem: An identifier string, usually in reverse DNS notation, representing the subsystem that’s performing logging (defaults to current process name).
/// - logLevelConversion: A dictionary keyed by TraceLog LogLevels with the value to convert to the os_log level.
///
public init(subsystem: String? = nil, logLevelConversion: [TraceLog.LogLevel: Platform.LogLevel] = defaultLogLevelConversion) {
self.subsystem = subsystem ?? ProcessInfo.processInfo.processName
self.logLevelConversion = logLevelConversion
}

///
/// Required log function for the `Writer`.
///
@inline(__always)
public func write(_ entry: Writer.LogEntry) -> Result<Int, FailureReason> {

let log = OSLog(subsystem: self.subsystem, category: entry.tag)

os_log("%{public}@", log: log, type: OSLogType(UInt8(platformLogLevel(for: entry.level))), entry.message)

return .success(entry.message.count)
}

///
/// Converts TraceLog level to os_log.
///
@inline(__always)
func platformLogLevel(for level: LogLevel) -> Platform.LogLevel {

guard let level = self.logLevelConversion[level]
else { return Platform.LogLevel(OSLogType.default.rawValue) }

return level
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,33 @@
#ifndef Pods_TraceLog_h
#define Pods_TraceLog_h

///
/// Swift Package Manager requires these to be declared otherwise, the
/// TraceLog module needs to be included with this header for Objective-C
/// which is undesierable.
///
#import <Foundation/Foundation.h>

/// Internal class exposed to objective-C for low level logging.
///
/// - Warning: This is a private class and nothing in this class should be used on it's own. Please see TraceLog.h for the public interface to this.
///
/// - Note: In order to continue to support Objective-C, this class must be public and also visible to both Swift and ObjC. This class is not meant to be
/// used directly in either language.
///
@interface TLLogger: NSObject

@property (class, nonatomic, readonly) NSInteger LogLevelError;
@property (class, nonatomic, readonly) NSInteger LogLevelWarning;
@property (class, nonatomic, readonly) NSInteger LogLevelInfo;
@property (class, nonatomic, readonly) NSInteger LogLevelTrace1;
@property (class, nonatomic, readonly) NSInteger LogLevelTrace2;
@property (class, nonatomic, readonly) NSInteger LogLevelTrace3;
@property (class, nonatomic, readonly) NSInteger LogLevelTrace4;

+ (void) logPrimitive: (NSInteger) level tag: (NSString * __nonnull) tag file: (NSString * __nonnull) file function: (NSString * __nonnull) function line: (NSUInteger) line message: (NSString * _Nonnull (^_Nonnull)(void)) messageBlock;
@end

/// Instance level macros

/**
Expand Down Expand Up @@ -175,33 +202,5 @@
*/
#define CLogTrace(level,tag,format,...) LogIfEnabled(TLLogger.LogLevelTrace1 + ((int)level) - 1, tag, format, ##__VA_ARGS__)

#if !COCOAPODS
///
/// Swift Package Manager requires these to be declared otherwise, the
/// TraceLog module needs to be included with this header for Objective-C
/// which is undesierable.
///
#import <Foundation/Foundation.h>

/// Internal class exposed to objective-C for low level logging.
///
/// - Warning: This is a private class and nothing in this class should be used on it's own. Please see TraceLog.h for the public interface to this.
///
/// - Note: In order to continue to support Objective-C, this class must be public and also visible to both Swift and ObjC. This class is not meant to be
/// used directly in either language.
///
@interface TLLogger: NSObject

@property (class, nonatomic, readonly) NSInteger LogLevelError;
@property (class, nonatomic, readonly) NSInteger LogLevelWarning;
@property (class, nonatomic, readonly) NSInteger LogLevelInfo;
@property (class, nonatomic, readonly) NSInteger LogLevelTrace1;
@property (class, nonatomic, readonly) NSInteger LogLevelTrace2;
@property (class, nonatomic, readonly) NSInteger LogLevelTrace3;
@property (class, nonatomic, readonly) NSInteger LogLevelTrace4;

+ (void) logPrimitive: (NSInteger) level tag: (NSString * __nonnull) tag file: (NSString * __nonnull) file function: (NSString * __nonnull) function line: (NSUInteger) line message: (NSString * _Nonnull (^_Nonnull)(void)) messageBlock;
@end
#endif

#endif
2 changes: 1 addition & 1 deletion Tests/TraceLogObjCTests/TraceLogObjCTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/
@import XCTest;

#import "TraceLog.h"
#import <TraceLog/TraceLog.h>

@interface TraceLogObjCTests : XCTestCase
@end
Expand Down
128 changes: 128 additions & 0 deletions Tests/TraceLogTests/TestHarness/UnifiedLogReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
///
/// DarwinPlatformValidator.swift
///
/// Copyright 2018 Tony Stone
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Created by Tony Stone on 6/16/18.
///
import XCTest
@testable import TraceLog

#if os(macOS)

import os.log


@available(iOS 10.0, macOS 10.13, watchOS 3.0, tvOS 10.0, *)
class UnifiedLogReader: Reader {

func logEntry(for writer: UnifiedLogWriter, timestamp: Double, level: LogLevel, tag: String, message: String, runtimeContext: RuntimeContext, staticContext: StaticContext) -> TestLogEntry? {

let osLogType = OSLogType(rawValue: UInt8(writer.platformLogLevel(for: level)))

/// If Unified Logging is not configured for this test, we fail early.
let log = OSLog(subsystem: writer.subsystem, category: tag)

guard log.isEnabled(type: osLogType)
else {

XCTFail("\n\nCannot complete log entry search.\n\n" +
"\tUnified Logging is not configured for this LogLevel.\n\n" +
"\tPlease run `sudo log config --subsystem \"\(writer.subsystem)\" --mode \"persist:debug\"` before running this test.\n")
return nil
}

let command = "log show --predicate 'eventMessage == \"\(message)\"' --info --debug --style json"

///
/// Note: Unified takes an undetermined time before log entries are available to
/// the log command so we try up to 10 times to find the value before giving up.
///
var retryTime: useconds_t = 1000

for _ in 0...10 {

guard let data = try? shell(command + " --last \(retryTime / 1000)")
else { XCTFail("Could not run shell command \(command + " --last \(retryTime / 1000)")."); return nil }

let objects: Any
do {
objects = try JSONSerialization.jsonObject(with: data)
} catch {
XCTFail("Could not parse JSON \(String(data: data, encoding: .utf8) ?? "nil"), error: \(error)."); return nil
}

guard let logEntries = objects as? [[String: Any]]
else {
XCTFail("Incorrect json object returned from parsing log show results, expected [[String: Any]] but got \(type(of: objects))."); return nil
}

guard logEntries.count > 0
else {
usleep(retryTime)
retryTime = retryTime * 2 /// progressivly sleep longer for each retry.

continue
}

/// Find the journal entry by message string (message string should be unique based on the string + timestamp).
for jsonEntry in logEntries where jsonEntry["eventMessage"] as? String ?? "" == message {

var customAttributes: [String: Any]? = nil

if let subsystem = jsonEntry["subsystem"] as? String {
customAttributes = ["subsystem": subsystem]
}

return TestLogEntry(timestamp: timestamp,
level: level,
message: message, // Note we return the input message because we know it matches because we searched by message
tag: jsonEntry["category"] as? String,
customAttributes: customAttributes)
// assertValue(for: jsonEntry, key: "messageType", eqauls: "\(osLogType.description)")
}
}
return nil
}
}

@available(iOS 10.0, macOS 10.13, watchOS 3.0, tvOS 10.0, *)
extension OSLogType {

public var description: String {
switch self {
case .fault: return "Fault"
case .error: return "Error"
case .debug: return "Debug"
default: return "Default"
}
}
}


@available(iOS 10.0, macOS 10.13, watchOS 3.0, tvOS 10.0, *)
class DarwinPlatformValidator {

static var `default`: Platform.LogLevel { return Platform.LogLevel(OSLogType.default.rawValue) }
static var error: Platform.LogLevel { return Platform.LogLevel(OSLogType.error.rawValue) }
static var warning: Platform.LogLevel { return Platform.LogLevel(OSLogType.default.rawValue) }
static var info: Platform.LogLevel { return Platform.LogLevel(OSLogType.default.rawValue) }
static var trace1: Platform.LogLevel { return Platform.LogLevel(OSLogType.debug.rawValue) }
static var trace2: Platform.LogLevel { return Platform.LogLevel(OSLogType.debug.rawValue) }
static var trace3: Platform.LogLevel { return Platform.LogLevel(OSLogType.debug.rawValue) }
static var trace4: Platform.LogLevel { return Platform.LogLevel(OSLogType.debug.rawValue) }
}

#endif
Loading