├── .github └── workflows │ └── main.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── src ├── lib.rs ├── logger.rs └── sys.rs └── wrapper.c /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Compile check 8 | runs-on: macOS-latest 9 | strategy: 10 | matrix: 11 | target: [x86_64-apple-darwin, aarch64-apple-darwin, aarch64-apple-ios] 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v2 15 | 16 | - name: Install stable toolchain 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: stable 21 | override: true 22 | target: ${{ matrix.target }} 23 | 24 | - name: Run cargo check 25 | uses: actions-rs/cargo@v1 26 | with: 27 | command: check 28 | args: --target ${{ matrix.target }} 29 | 30 | test: 31 | name: Test Suite 32 | runs-on: macos-latest 33 | steps: 34 | - name: Checkout sources 35 | uses: actions/checkout@v2 36 | 37 | - name: Install stable toolchain 38 | uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: stable 42 | override: true 43 | 44 | - name: Run cargo test 45 | uses: actions-rs/cargo@v1 46 | with: 47 | command: test 48 | 49 | lints: 50 | name: Lints 51 | runs-on: macos-latest 52 | steps: 53 | - name: Checkout sources 54 | uses: actions/checkout@v2 55 | 56 | - name: Install stable toolchain 57 | uses: actions-rs/toolchain@v1 58 | with: 59 | profile: minimal 60 | toolchain: stable 61 | override: true 62 | components: rustfmt, clippy 63 | 64 | - name: Run cargo fmt 65 | uses: actions-rs/cargo@v1 66 | with: 67 | command: fmt 68 | args: --all -- --check 69 | 70 | - name: Run cargo clippy 71 | uses: actions-rs/cargo@v1 72 | with: 73 | command: clippy 74 | args: -- -D warnings 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oslog" 3 | description = "A minimal safe wrapper around Apple's Logging system" 4 | repository = "https://github.com/steven-joruk/oslog" 5 | version = "0.2.0" 6 | authors = ["Steven Joruk "] 7 | edition = "2021" 8 | license = "MIT" 9 | readme = "README.md" 10 | keywords = ["log", "logging", "macos", "apple"] 11 | categories = ["development-tools::debugging"] 12 | 13 | [features] 14 | default = ["logger"] 15 | 16 | # Enables support for the `log` crate. 17 | logger = ["dashmap", "log"] 18 | 19 | [dependencies] 20 | log = { version = "0.4.14", default-features = false, features = ["std"], optional = true } 21 | dashmap = { version = "5.1.0", optional = true } 22 | 23 | [build-dependencies] 24 | cc = "1.0.73" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steven Joruk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crate](https://img.shields.io/crates/v/oslog.svg)](https://crates.io/crates/oslog) 2 | 3 | A minimal wrapper around Apple's unified logging system. 4 | 5 | By default support for the [log](https://docs.rs/log) crate is provided, but if 6 | you would prefer just to use the lower level bindings you can disable the 7 | default features. 8 | 9 | When making use of targets (`info!(target: "t", "m");`), you should be aware 10 | that a new log is allocated and stored in a map for the lifetime of the program. 11 | I expect log allocations are extremely small, but haven't attempted to verify 12 | it. 13 | 14 | ## Logging example 15 | 16 | This is behind the `logger` feature flag and is enabled by default. 17 | 18 | ```rust 19 | fn main() { 20 | OsLogger::new("com.example.test") 21 | .level_filter(LevelFilter::Debug) 22 | .category_level_filter("Settings", LevelFilter::Trace) 23 | .init() 24 | .unwrap(); 25 | 26 | // Maps to OS_LOG_TYPE_DEBUG 27 | trace!(target: "Settings", "Trace"); 28 | 29 | // Maps to OS_LOG_TYPE_INFO 30 | debug!("Debug"); 31 | 32 | // Maps to OS_LOG_TYPE_DEFAULT 33 | info!(target: "Parsing", "Info"); 34 | 35 | // Maps to OS_LOG_TYPE_ERROR 36 | warn!("Warn"); 37 | 38 | // Maps to OS_LOG_TYPE_FAULT 39 | error!("Error"); 40 | } 41 | ``` 42 | 43 | ## Limitations 44 | 45 | Most of Apple's logging related functions are macros that enable some 46 | optimizations as well as providing contextual data such as source file location. 47 | 48 | By wrapping the macros for use from Rust, we lose those benefits. 49 | 50 | Attempting to work around this would involve digging in to opaque types, which 51 | would be an automatic or eventual rejection from the App store. 52 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | cc::Build::new().file("wrapper.c").compile("wrapper"); 3 | } 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod sys; 2 | 3 | #[cfg(feature = "logger")] 4 | mod logger; 5 | 6 | #[cfg(feature = "logger")] 7 | pub use logger::OsLogger; 8 | 9 | use crate::sys::*; 10 | use std::ffi::{c_void, CString}; 11 | 12 | #[inline] 13 | fn to_cstr(message: &str) -> CString { 14 | let fixed = message.replace('\0', "(null)"); 15 | CString::new(fixed).unwrap() 16 | } 17 | 18 | #[repr(u8)] 19 | pub enum Level { 20 | Debug = OS_LOG_TYPE_DEBUG, 21 | Info = OS_LOG_TYPE_INFO, 22 | Default = OS_LOG_TYPE_DEFAULT, 23 | Error = OS_LOG_TYPE_ERROR, 24 | Fault = OS_LOG_TYPE_FAULT, 25 | } 26 | 27 | #[cfg(feature = "logger")] 28 | impl From for Level { 29 | fn from(other: log::Level) -> Self { 30 | match other { 31 | log::Level::Trace => Self::Debug, 32 | log::Level::Debug => Self::Info, 33 | log::Level::Info => Self::Default, 34 | log::Level::Warn => Self::Error, 35 | log::Level::Error => Self::Fault, 36 | } 37 | } 38 | } 39 | 40 | pub struct OsLog { 41 | inner: os_log_t, 42 | } 43 | 44 | unsafe impl Send for OsLog {} 45 | unsafe impl Sync for OsLog {} 46 | 47 | impl Drop for OsLog { 48 | fn drop(&mut self) { 49 | unsafe { 50 | if self.inner != wrapped_get_default_log() { 51 | os_release(self.inner as *mut c_void); 52 | } 53 | } 54 | } 55 | } 56 | 57 | impl OsLog { 58 | #[inline] 59 | pub fn new(subsystem: &str, category: &str) -> Self { 60 | let subsystem = to_cstr(subsystem); 61 | let category = to_cstr(category); 62 | 63 | let inner = unsafe { os_log_create(subsystem.as_ptr(), category.as_ptr()) }; 64 | 65 | assert!(!inner.is_null(), "Unexpected null value from os_log_create"); 66 | 67 | Self { inner } 68 | } 69 | 70 | #[inline] 71 | pub fn global() -> Self { 72 | let inner = unsafe { wrapped_get_default_log() }; 73 | 74 | assert!(!inner.is_null(), "Unexpected null value for OS_DEFAULT_LOG"); 75 | 76 | Self { inner } 77 | } 78 | 79 | #[inline] 80 | pub fn with_level(&self, level: Level, message: &str) { 81 | let message = to_cstr(message); 82 | unsafe { wrapped_os_log_with_type(self.inner, level as u8, message.as_ptr()) } 83 | } 84 | 85 | #[inline] 86 | pub fn debug(&self, message: &str) { 87 | let message = to_cstr(message); 88 | unsafe { wrapped_os_log_debug(self.inner, message.as_ptr()) } 89 | } 90 | 91 | #[inline] 92 | pub fn info(&self, message: &str) { 93 | let message = to_cstr(message); 94 | unsafe { wrapped_os_log_info(self.inner, message.as_ptr()) } 95 | } 96 | 97 | #[inline] 98 | pub fn default(&self, message: &str) { 99 | let message = to_cstr(message); 100 | unsafe { wrapped_os_log_default(self.inner, message.as_ptr()) } 101 | } 102 | 103 | #[inline] 104 | pub fn error(&self, message: &str) { 105 | let message = to_cstr(message); 106 | unsafe { wrapped_os_log_error(self.inner, message.as_ptr()) } 107 | } 108 | 109 | #[inline] 110 | pub fn fault(&self, message: &str) { 111 | let message = to_cstr(message); 112 | unsafe { wrapped_os_log_fault(self.inner, message.as_ptr()) } 113 | } 114 | 115 | #[inline] 116 | pub fn level_is_enabled(&self, level: Level) -> bool { 117 | unsafe { os_log_type_enabled(self.inner, level as u8) } 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | 125 | #[test] 126 | fn test_subsystem_interior_null() { 127 | let log = OsLog::new("com.example.oslog\0test", "category"); 128 | log.with_level(Level::Debug, "Hi"); 129 | } 130 | 131 | #[test] 132 | fn test_category_interior_null() { 133 | let log = OsLog::new("com.example.oslog", "category\0test"); 134 | log.with_level(Level::Debug, "Hi"); 135 | } 136 | 137 | #[test] 138 | fn test_message_interior_null() { 139 | let log = OsLog::new("com.example.oslog", "category"); 140 | log.with_level(Level::Debug, "Hi\0test"); 141 | } 142 | 143 | #[test] 144 | fn test_message_emoji() { 145 | let log = OsLog::new("com.example.oslog", "category"); 146 | log.with_level(Level::Debug, "\u{1F601}"); 147 | } 148 | 149 | #[test] 150 | fn test_global_log_with_level() { 151 | let log = OsLog::global(); 152 | log.with_level(Level::Debug, "Debug"); 153 | log.with_level(Level::Info, "Info"); 154 | log.with_level(Level::Default, "Default"); 155 | log.with_level(Level::Error, "Error"); 156 | log.with_level(Level::Fault, "Fault"); 157 | } 158 | 159 | #[test] 160 | fn test_global_log() { 161 | let log = OsLog::global(); 162 | log.debug("Debug"); 163 | log.info("Info"); 164 | log.default("Default"); 165 | log.error("Error"); 166 | log.fault("Fault"); 167 | } 168 | 169 | #[test] 170 | fn test_custom_log_with_level() { 171 | let log = OsLog::new("com.example.oslog", "testing"); 172 | log.with_level(Level::Debug, "Debug"); 173 | log.with_level(Level::Info, "Info"); 174 | log.with_level(Level::Default, "Default"); 175 | log.with_level(Level::Error, "Error"); 176 | log.with_level(Level::Fault, "Fault"); 177 | } 178 | 179 | #[test] 180 | fn test_custom_log() { 181 | let log = OsLog::new("com.example.oslog", "testing"); 182 | log.debug("Debug"); 183 | log.info("Info"); 184 | log.default("Default"); 185 | log.error("Error"); 186 | log.fault("Fault"); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use crate::OsLog; 2 | use dashmap::DashMap; 3 | use log::{LevelFilter, Log, Metadata, Record}; 4 | 5 | pub struct OsLogger { 6 | loggers: DashMap, OsLog)>, 7 | subsystem: String, 8 | } 9 | 10 | impl Log for OsLogger { 11 | fn enabled(&self, metadata: &Metadata) -> bool { 12 | let max_level = self 13 | .loggers 14 | .get(metadata.target()) 15 | .and_then(|pair| pair.0) 16 | .unwrap_or_else(log::max_level); 17 | 18 | metadata.level() <= max_level 19 | } 20 | 21 | fn log(&self, record: &Record) { 22 | if self.enabled(record.metadata()) { 23 | let pair = self 24 | .loggers 25 | .entry(record.target().into()) 26 | .or_insert((None, OsLog::new(&self.subsystem, record.target()))); 27 | 28 | let message = std::format!("{}", record.args()); 29 | pair.1.with_level(record.level().into(), &message); 30 | } 31 | } 32 | 33 | fn flush(&self) {} 34 | } 35 | 36 | impl OsLogger { 37 | /// Creates a new logger. You must also call `init` to finalize the set up. 38 | /// By default the level filter will be set to `LevelFilter::Trace`. 39 | pub fn new(subsystem: &str) -> Self { 40 | Self { 41 | loggers: DashMap::new(), 42 | subsystem: subsystem.to_string(), 43 | } 44 | } 45 | 46 | /// Only levels at or above `level` will be logged. 47 | pub fn level_filter(self, level: LevelFilter) -> Self { 48 | log::set_max_level(level); 49 | self 50 | } 51 | 52 | /// Sets or updates the category's level filter. 53 | pub fn category_level_filter(self, category: &str, level: LevelFilter) -> Self { 54 | self.loggers 55 | .entry(category.into()) 56 | .and_modify(|(existing_level, _)| *existing_level = Some(level)) 57 | .or_insert((Some(level), OsLog::new(&self.subsystem, category))); 58 | 59 | self 60 | } 61 | 62 | pub fn init(self) -> Result<(), log::SetLoggerError> { 63 | log::set_boxed_logger(Box::new(self)) 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use super::*; 70 | use log::{debug, error, info, trace, warn}; 71 | 72 | #[test] 73 | fn test_basic_usage() { 74 | OsLogger::new("com.example.oslog") 75 | .level_filter(LevelFilter::Trace) 76 | .category_level_filter("Settings", LevelFilter::Warn) 77 | .category_level_filter("Database", LevelFilter::Error) 78 | .category_level_filter("Database", LevelFilter::Trace) 79 | .init() 80 | .unwrap(); 81 | 82 | // This will not be logged because of its category's custom level filter. 83 | info!(target: "Settings", "Info"); 84 | 85 | warn!(target: "Settings", "Warn"); 86 | error!(target: "Settings", "Error"); 87 | 88 | trace!("Trace"); 89 | debug!("Debug"); 90 | info!("Info"); 91 | warn!(target: "Database", "Warn"); 92 | error!("Error"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/sys.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(dead_code)] 3 | 4 | use std::{ffi::c_void, os::raw::c_char}; 5 | 6 | #[repr(C)] 7 | #[derive(Debug, Copy, Clone)] 8 | pub struct os_log_s { 9 | _unused: [u8; 0], 10 | } 11 | 12 | pub type os_log_t = *mut os_log_s; 13 | pub type os_log_type_t = u8; 14 | 15 | pub const OS_LOG_TYPE_DEFAULT: os_log_type_t = 0; 16 | pub const OS_LOG_TYPE_INFO: os_log_type_t = 1; 17 | pub const OS_LOG_TYPE_DEBUG: os_log_type_t = 2; 18 | pub const OS_LOG_TYPE_ERROR: os_log_type_t = 16; 19 | pub const OS_LOG_TYPE_FAULT: os_log_type_t = 17; 20 | 21 | // Provided by the OS. 22 | extern "C" { 23 | pub fn os_log_create(subsystem: *const c_char, category: *const c_char) -> os_log_t; 24 | pub fn os_release(object: *mut c_void); 25 | pub fn os_log_type_enabled(log: os_log_t, level: os_log_type_t) -> bool; 26 | } 27 | 28 | // Wrappers defined in wrapper.c because most of the os_log_* APIs are macros. 29 | extern "C" { 30 | pub fn wrapped_get_default_log() -> os_log_t; 31 | pub fn wrapped_os_log_with_type(log: os_log_t, log_type: os_log_type_t, message: *const c_char); 32 | pub fn wrapped_os_log_debug(log: os_log_t, message: *const c_char); 33 | pub fn wrapped_os_log_info(log: os_log_t, message: *const c_char); 34 | pub fn wrapped_os_log_default(log: os_log_t, message: *const c_char); 35 | pub fn wrapped_os_log_error(log: os_log_t, message: *const c_char); 36 | pub fn wrapped_os_log_fault(log: os_log_t, message: *const c_char); 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | use std::ffi::CString; 43 | 44 | #[test] 45 | fn test_create_and_release() { 46 | let subsystem = CString::new("com.example.test").unwrap(); 47 | let category = CString::new("category").unwrap(); 48 | let log = unsafe { os_log_create(subsystem.as_ptr(), category.as_ptr()) }; 49 | assert!(!log.is_null()); 50 | 51 | unsafe { 52 | os_release(log as *mut _); 53 | } 54 | } 55 | 56 | #[test] 57 | fn test_output_to_default_log() { 58 | let message = CString::new("Hello!").unwrap(); 59 | 60 | unsafe { 61 | wrapped_os_log_debug(wrapped_get_default_log(), message.as_ptr()); 62 | wrapped_os_log_info(wrapped_get_default_log(), message.as_ptr()); 63 | wrapped_os_log_default(wrapped_get_default_log(), message.as_ptr()); 64 | wrapped_os_log_error(wrapped_get_default_log(), message.as_ptr()); 65 | wrapped_os_log_fault(wrapped_get_default_log(), message.as_ptr()); 66 | 67 | wrapped_os_log_with_type( 68 | wrapped_get_default_log(), 69 | OS_LOG_TYPE_DEBUG, 70 | message.as_ptr(), 71 | ); 72 | wrapped_os_log_with_type( 73 | wrapped_get_default_log(), 74 | OS_LOG_TYPE_INFO, 75 | message.as_ptr(), 76 | ); 77 | wrapped_os_log_with_type( 78 | wrapped_get_default_log(), 79 | OS_LOG_TYPE_DEFAULT, 80 | message.as_ptr(), 81 | ); 82 | wrapped_os_log_with_type( 83 | wrapped_get_default_log(), 84 | OS_LOG_TYPE_ERROR, 85 | message.as_ptr(), 86 | ); 87 | wrapped_os_log_with_type( 88 | wrapped_get_default_log(), 89 | OS_LOG_TYPE_FAULT, 90 | message.as_ptr(), 91 | ); 92 | } 93 | } 94 | 95 | #[test] 96 | fn test_output_to_custom_log() { 97 | let subsystem = CString::new("com.example.test").unwrap(); 98 | let category = CString::new("category").unwrap(); 99 | let log = unsafe { os_log_create(subsystem.as_ptr(), category.as_ptr()) }; 100 | let message = CString::new("Hello!").unwrap(); 101 | 102 | unsafe { 103 | wrapped_os_log_debug(log, message.as_ptr()); 104 | wrapped_os_log_info(log, message.as_ptr()); 105 | wrapped_os_log_default(log, message.as_ptr()); 106 | wrapped_os_log_error(log, message.as_ptr()); 107 | wrapped_os_log_fault(log, message.as_ptr()); 108 | 109 | wrapped_os_log_with_type(log, OS_LOG_TYPE_DEBUG, message.as_ptr()); 110 | wrapped_os_log_with_type(log, OS_LOG_TYPE_INFO, message.as_ptr()); 111 | wrapped_os_log_with_type(log, OS_LOG_TYPE_DEFAULT, message.as_ptr()); 112 | wrapped_os_log_with_type(log, OS_LOG_TYPE_ERROR, message.as_ptr()); 113 | wrapped_os_log_with_type(log, OS_LOG_TYPE_FAULT, message.as_ptr()); 114 | 115 | os_release(log as *mut _); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /wrapper.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | os_log_t wrapped_get_default_log() { 4 | return OS_LOG_DEFAULT; 5 | } 6 | 7 | void wrapped_os_log_with_type(os_log_t log, os_log_type_t type, const char* message) { 8 | os_log_with_type(log, type, "%{public}s", message); 9 | } 10 | 11 | void wrapped_os_log_debug(os_log_t log, const char* message) { 12 | os_log_debug(log, "%{public}s", message); 13 | } 14 | 15 | void wrapped_os_log_info(os_log_t log, const char* message) { 16 | os_log_info(log, "%{public}s", message); 17 | } 18 | 19 | void wrapped_os_log_default(os_log_t log, const char* message) { 20 | os_log(log, "%{public}s", message); 21 | } 22 | 23 | void wrapped_os_log_error(os_log_t log, const char* message) { 24 | os_log_error(log, "%{public}s", message); 25 | } 26 | 27 | void wrapped_os_log_fault(os_log_t log, const char* message) { 28 | os_log_fault(log, "%{public}s", message); 29 | } --------------------------------------------------------------------------------