Utility class that polls OSLogStore and sends any valid logs to subscribed log drivers.
OSLog
is the recommended logging approach by Apple and the core Swift team. A comprehensive and reliable logging system is essential in software development. However, projects often need to send logs to third-party vendors or other services. This presents a challenge: how to use OSLog
while also ensuring seamless and flexible integration with external logging solutions.
OSLogClient
aims to bridge this gap by acting as an intermediary, channeling the strengths and convenience of OSLog
into custom logging mechanisms. It does this by polling the underlying OSLogStore
, assessing the logs, and then forwarding the post-processed log messages to registered LogDriver
instances. As a result, a registered LogDriver
can receive log messages with all OSLog
-based privacy, security, and formatting intact. The driver will also receive metadata such as date-time, log level, logger subsystem, and logger category.
With the increasing emphasis on user data privacy and security, Apple's OSLog
has become an invaluable tool for developers. OSLog
provides many advantages:
OSLog allows you to format and redact sensitive data, ensuring user data isn't unintentionally exposed.
OSLog was designed for efficiency, it minimizes the performance impact on your apps.
OSLog Integrates seamlessly with the system's diagnostic framework, making troubleshooting easier. You can use the native Console app to filter and monitor logs far easier.
Apple and the core Swift team recommend using OSLog
over other logging mechanisms due to its in-built capabilities.
By integrating OSLog
with our library, you are enabled to harness the strengths of OSLog while ensuring a flexible logging infrastructure that can be extended as per your project needs.
Using the OSLogClient
is straightforward. Below is a simple guide to get you started:
// Import the library (OSLog is also included in the import)
import OSLogClient
// Initialize the OSLogClient
try OSLogClient.initialize(pollingInterval: .short)
// Register your custom log driver
let myDriver = MyLogDriver(id: "myLogDriver")
await OSLogClient.registerDriver(myDriver)
// Start polling
await OSLogClient.startPolling()
Note: If you are not using structured concurrency yet (or still adopting etc) you will need to run your registrations and invocations within a task. Below is a contrived example, you should consider task lifecycle and status etc:
try OSLogClient.initialize(pollingInterval: .short)
let setupTask = Task(priority: .userInitiated) {
// Register your custom log driver
let myDriver = MyLogDriver(id: "myLogDriver")
await OSLogClient.registerDriver(myDriver)
// Start polling
await OSLogClient.startPolling()
}
With just these three steps, OSLogClient
begins monitoring logs from OSLog
and forwards them to your registered log drivers, leaving you to use OSLog.Logger
instances as normal:
let logger = Logger(subsystem: "com.company.AppName", category: "ui")
logger.info("Password '\(password, privacy: .private)' did not pass validation")
when your driver gets the log message, it will be the processed message that ensures any privacy and formatting has been applied. For example, when not attached to a debugger, the above would invoke with:
"Password '<private>' did not pass validation"
While the intended usage of the library is to use the OSLogClient
entry point, you can initialize your own intance of the LogClient
type to maintain yourself:
let logStore = OSLogStore(scope: .currentProcessIdentifier)
let client = try LogClient(pollingInterval: .medium, logStore: logStore)
Note: This is provided for those edge case scenarios where you need to work with your own instance/s. Please keep in mind that the OSLogClient
entry point will still be functional and available in these setups.
While the base LogDriver class provides the necessary foundation for handling OS logs, you can easily subclass it for custom processing, such as writing logs to a text file:
import OSLogClient
class TextLogDriver: LogDriver {
// MARK: - Properties
var logFileUrl: URL
// MARK: - Lifecycle
required init(id: String, logFileUrl: URL, logFilters: [LogFilter] = []) {
self.logFileUrl = logFileUrl
super.init(id: id, logFilters: logFilters)
}
required init(id: String, logFilters: [LogFilter] = []) {
fatalError("init(id:logFilters:) has not been implemented")
}
// MARK: - Overrides
#if os(macOS)
override func processLog(level: LogDriver.LogLevel, subsystem: String, category: String, date: Date, message: String, components: [OSLogMessageComponent]) {
formatAndWriteMessage(level: level, category: category, date: date, message: message)
}
#else
override func processLog(level: LogDriver.LogLevel, subsystem: String, category: String, date: Date, message: String) {
formatAndWriteMessage(level: level, category: category, date: date, message: message)
}
#endif
// MARK: - Helpers
func formatAndWriteMessage(level: LogLevel, category: String, date: Date, message: String) {
let message = "[\(category)-\(level.rawValue.uppercased())]: \(date): \(message)"
var contents = (try? String(contentsOf: logFileUrl).trimmingCharacters(in: .whitespacesAndNewlines)) ?? ""
contents += "\(contents.isEmpty ? "" : "\n")\(message)"
try? contents.write(to: logFileUrl, atomically: true, encoding: .utf8)
}
}
Instead of only assessing log level, date, and category in the processLog
method, you can fine-tune which logs should be processed by a LogDriver
instance by specifying valid LogFilter
conditions.
If log filters are specified (i.e., the list isn't empty), they're used to evaluate incoming log entries, ensuring there's a matching filter.
See the LogFilter
and LogCategoryFilter
documentation for supported options.
For example, to configure a log driver to only receive ui
and api
log entries:
let apiLogger = Logger(subsystem: "com.company.AppName", category: "api")
let uiLogger = Logger(subsystem: "com.company.AppName", category: "ui")
let storageLogger = Logger(subsystem: "com.vendor.AppName", category: "storage")
myLogDriver.addLogFilters([
.subsystem("com.company.AppName", categories: "ui", "api")
])
With this setup, logger instances work as usual, but the driver will only capture logs validated by at least one log source:
// Driver will capture these logs:
apiLogger.info("api info message")
uiLogger.info("button was tapped")
// Driver **won't** capture this log:
storageLogger.error("database error message")
This approach facilitates managing loggers with varied categories across distinct driver instances as needed.
The PollingInterval
supports four enumerations:
.short // 10 second intervals
.medium // 30 second intervals
.long // 60 second intervals
.custom(TimeInterval) // Poll at the given duration (in seconds)
Note: There is a hard-enforced minimum of 1 second for the custom
interval option.
You can also request a poll of logs from a given point in time. The date to poll from is optional and defaults to time of the most recently polled log:
OSLogClient.pollImmediately() // Use last processed
OSLogClient.pollImmediately(from: customDate) // Custom point in time
Currently, OSLogClient supports Swift Package Manager (SPM).
To add OSLogClient to your project, add the following line to your dependencies in your Package.swift file:
.package(url: "https://github.com/CheekyGhost-Labs/OSLogClient", from: "1.0.0")
Then, add OSLogClient as a dependency for your target:
.target(
name: "YourTarget",
dependencies: [
// other dependencies
.product(name: "OSLogClient", package: "OSLogClient")
]
),
OSLogClient is released under the MIT License. See the LICENSE file for more information.
Contributions to OSLogClient are welcomed! If you have a bug to report, feel free to help out by opening a new issue or submitting a pull request.
OSLogClient follows pretty closely to a standard git flow process. For the most part, pull requests should be made against the develop
branch to coordinate any releases. This also provides a means to test from the develop
branch in the wild to further test pending releases. Once a release is ready it will be merged into main
, tagged, and have a release branch cut.
-
Fork the repository: Start by creating a fork of the project to your own GitHub account.
-
Clone the forked repository: After forking, clone your forked repository to your local machine so you can make changes.
git clone https://github.com/CheekyGhost-Labs/OSLogClient.git
- Create a new branch: Before making changes, create a new branch for your feature or bug fix. Use a descriptive name that reflects the purpose of your changes.
git checkout -b your-feature-branch
-
Follow the Swift Language Guide: Ensure that your code adheres to the Swift Language Guide for styling and syntax conventions.
-
Make your changes: Implement your feature or bug fix, following the project's code style and best practices. Don't forget to add tests and update documentation as needed.
-
Commit your changes: Commit your changes with a descriptive and concise commit message. Use the imperative mood, and explain what your commit does, rather than what you did.
# Feature
git commit -m "Feature: Adding convenience method of awesomeness"
# Bug
git commit -m "Bug: Fixing issue where awesome thing was not including awesome"
- Pull the latest changes from the upstream: Before submitting your changes, make sure to pull the latest changes from the upstream repository and merge them into your branch. This helps to avoid any potential merge conflicts.
git pull origin develop
- Push your changes: Push your changes to your forked repository on GitHub.
git push origin your-feature-branch
- Submit a pull request: Finally, create a pull request from your forked repository to the original repository, targeting the
develop
branch. Fill in the pull request template with the necessary details, and wait for the project maintainers to review your contribution.
Please ensure you add unit tests for any changes. The aim is not 100%
coverage, but rather meaningful test coverage that ensures your changes are behaving as expected without negatively effecting existing behavior.
Please note that the project maintainers may ask you to make changes to your contribution or provide additional information. Be open to feedback and willing to make adjustments as needed. Once your pull request is approved and merged, your changes will become part of the project!
For a deeper dive into Apple's OSLog, please refer to the following documentation: