Skip to content

Commit

Permalink
Rewrite to improve performance and remove dashmap
Browse files Browse the repository at this point in the history
  • Loading branch information
steven-joruk committed Jul 24, 2024
1 parent f192e27 commit e1cd875
Show file tree
Hide file tree
Showing 17 changed files with 455 additions and 320 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jobs:
strategy:
matrix:
target: [x86_64-apple-darwin, aarch64-apple-darwin, aarch64-apple-ios]
features-flags: ["", "-Flog", "-Fcategories"]
steps:
- name: Checkout sources
uses: actions/checkout@v2
Expand All @@ -25,7 +26,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: check
args: --target ${{ matrix.target }}
args: --target ${{ matrix.target }} ${{ matrix.feature-flags }}

test:
name: Test Suite
Expand Down
11 changes: 7 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ keywords = ["log", "logging", "macos", "apple"]
categories = ["development-tools::debugging"]

[features]
default = ["logger"]

# Enables support for the `log` crate.
logger = ["dashmap", "log"]
default = ["categories"]

# Enables support for the log crate.
log = ["dep:log"]

# Supports creating a new log for each category. This introduces a Mutex.
categories = ["log"]

[dependencies]
log = { version = "0.4.14", default-features = false, features = ["std"], optional = true }
dashmap = { version = "5.1.0", optional = true }

[build-dependencies]
cc = "1.0.73"
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ fn main() {
}
```

## Features

* `log` enables [log](https://docs.rs/log/latest/log/) crate support via
`OsLogger`.
* `categories` enables dynamically creating a log for each category. This
introduces a mutex which slows logging down.

## Limitations

Most of Apple's logging related functions are macros that enable some
Expand All @@ -49,3 +56,6 @@ By wrapping the macros for use from Rust, we lose those benefits.

Attempting to work around this would involve digging in to opaque types, which
would be an automatic or eventual rejection from the App store.

We also lose performance by having to convert log messages to C strings, even if
there's no observer of in-memory log streams.
12 changes: 12 additions & 0 deletions src/level.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::sys::{
OS_LOG_TYPE_DEBUG, OS_LOG_TYPE_DEFAULT, OS_LOG_TYPE_ERROR, OS_LOG_TYPE_FAULT, OS_LOG_TYPE_INFO,
};

#[repr(u8)]
pub enum Level {
Debug = OS_LOG_TYPE_DEBUG,
Info = OS_LOG_TYPE_INFO,
Default = OS_LOG_TYPE_DEFAULT,
Error = OS_LOG_TYPE_ERROR,
Fault = OS_LOG_TYPE_FAULT,
}
193 changes: 9 additions & 184 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,188 +1,13 @@
mod level;
mod minimal;
mod os_log_string;
mod sys;

#[cfg(feature = "logger")]
mod logger;
#[cfg(feature = "log")]
mod log;

#[cfg(feature = "logger")]
pub use logger::OsLogger;
#[cfg(feature = "log")]
pub use log::OsLogger;

use crate::sys::*;
use std::ffi::{c_void, CString};

#[inline]
fn to_cstr(message: &str) -> CString {
let fixed = message.replace('\0', "(null)");
CString::new(fixed).unwrap()
}

#[repr(u8)]
pub enum Level {
Debug = OS_LOG_TYPE_DEBUG,
Info = OS_LOG_TYPE_INFO,
Default = OS_LOG_TYPE_DEFAULT,
Error = OS_LOG_TYPE_ERROR,
Fault = OS_LOG_TYPE_FAULT,
}

#[cfg(feature = "logger")]
impl From<log::Level> for Level {
fn from(other: log::Level) -> Self {
match other {
log::Level::Trace => Self::Debug,
log::Level::Debug => Self::Info,
log::Level::Info => Self::Default,
log::Level::Warn => Self::Error,
log::Level::Error => Self::Fault,
}
}
}

pub struct OsLog {
inner: os_log_t,
}

unsafe impl Send for OsLog {}
unsafe impl Sync for OsLog {}

impl Drop for OsLog {
fn drop(&mut self) {
unsafe {
if self.inner != wrapped_get_default_log() {
os_release(self.inner as *mut c_void);
}
}
}
}

impl OsLog {
#[inline]
pub fn new(subsystem: &str, category: &str) -> Self {
let subsystem = to_cstr(subsystem);
let category = to_cstr(category);

let inner = unsafe { os_log_create(subsystem.as_ptr(), category.as_ptr()) };

assert!(!inner.is_null(), "Unexpected null value from os_log_create");

Self { inner }
}

#[inline]
pub fn global() -> Self {
let inner = unsafe { wrapped_get_default_log() };

assert!(!inner.is_null(), "Unexpected null value for OS_DEFAULT_LOG");

Self { inner }
}

#[inline]
pub fn with_level(&self, level: Level, message: &str) {
let message = to_cstr(message);
unsafe { wrapped_os_log_with_type(self.inner, level as u8, message.as_ptr()) }
}

#[inline]
pub fn debug(&self, message: &str) {
let message = to_cstr(message);
unsafe { wrapped_os_log_debug(self.inner, message.as_ptr()) }
}

#[inline]
pub fn info(&self, message: &str) {
let message = to_cstr(message);
unsafe { wrapped_os_log_info(self.inner, message.as_ptr()) }
}

#[inline]
pub fn default(&self, message: &str) {
let message = to_cstr(message);
unsafe { wrapped_os_log_default(self.inner, message.as_ptr()) }
}

#[inline]
pub fn error(&self, message: &str) {
let message = to_cstr(message);
unsafe { wrapped_os_log_error(self.inner, message.as_ptr()) }
}

#[inline]
pub fn fault(&self, message: &str) {
let message = to_cstr(message);
unsafe { wrapped_os_log_fault(self.inner, message.as_ptr()) }
}

#[inline]
pub fn level_is_enabled(&self, level: Level) -> bool {
unsafe { os_log_type_enabled(self.inner, level as u8) }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_subsystem_interior_null() {
let log = OsLog::new("com.example.oslog\0test", "category");
log.with_level(Level::Debug, "Hi");
}

#[test]
fn test_category_interior_null() {
let log = OsLog::new("com.example.oslog", "category\0test");
log.with_level(Level::Debug, "Hi");
}

#[test]
fn test_message_interior_null() {
let log = OsLog::new("com.example.oslog", "category");
log.with_level(Level::Debug, "Hi\0test");
}

#[test]
fn test_message_emoji() {
let log = OsLog::new("com.example.oslog", "category");
log.with_level(Level::Debug, "\u{1F601}");
}

#[test]
fn test_global_log_with_level() {
let log = OsLog::global();
log.with_level(Level::Debug, "Debug");
log.with_level(Level::Info, "Info");
log.with_level(Level::Default, "Default");
log.with_level(Level::Error, "Error");
log.with_level(Level::Fault, "Fault");
}

#[test]
fn test_global_log() {
let log = OsLog::global();
log.debug("Debug");
log.info("Info");
log.default("Default");
log.error("Error");
log.fault("Fault");
}

#[test]
fn test_custom_log_with_level() {
let log = OsLog::new("com.example.oslog", "testing");
log.with_level(Level::Debug, "Debug");
log.with_level(Level::Info, "Info");
log.with_level(Level::Default, "Default");
log.with_level(Level::Error, "Error");
log.with_level(Level::Fault, "Fault");
}

#[test]
fn test_custom_log() {
let log = OsLog::new("com.example.oslog", "testing");
log.debug("Debug");
log.info("Info");
log.default("Default");
log.error("Error");
log.fault("Fault");
}
}
pub use level::Level;
pub use minimal::OsLog;
11 changes: 11 additions & 0 deletions src/log/level.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
impl From<log::Level> for crate::Level {
fn from(other: log::Level) -> Self {
match other {
log::Level::Trace => Self::Debug,
log::Level::Debug => Self::Info,
log::Level::Info => Self::Default,
log::Level::Warn => Self::Error,
log::Level::Error => Self::Fault,
}
}
}
76 changes: 76 additions & 0 deletions src/log/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use crate::os_log_string::OsLogString;

use super::Logs;
use log::{LevelFilter, Log, Metadata, Record};
use std::ffi::CString;

pub struct OsLogger {
logs: Logs,
subsystem: CString,
}

impl Log for OsLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
let max_level = self
.logs
.max_level(&self.subsystem, metadata.target())
.unwrap_or_else(log::max_level);

metadata.level() <= max_level
}

fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let category = record.target();

self.logs
.with_log(&self.subsystem, category, |(_level_filter, log)| {
#[cfg(feature = "categories")]
let message = std::format!("{}", record.args());

#[cfg(not(feature = "categories"))]
let message = std::format!("[{category}] {}", record.args());

log.with_level(record.level().into(), &message);
});
}
}

fn flush(&self) {}
}

impl OsLogger {
/// Creates a new logger. You must also call `init` to finalize the set up.
/// By default the level filter will be set to `LevelFilter::Trace`.
pub fn new<S: OsLogString + ?Sized>(subsystem: &S) -> Self {
subsystem.with_cstr(|s| {
let subsystem = subsystem.to_owned();

Self {
logs: Logs::new(subsystem),
subsystem: s.to_owned(),
}
})
}

/// Only levels at or above `level` will be logged.
pub fn level_filter(self, level: LevelFilter) -> Self {
log::set_max_level(level);
self
}

/// Sets or updates the category's level filter.
#[allow(unused_mut)]
pub fn category_level_filter(mut self, category: &str, level: LevelFilter) -> Self {
self.logs
.with_log_mut(&self.subsystem, category, |(level_filter, _log)| {
*level_filter = Some(level);
});

self
}

pub fn init(self) -> Result<(), log::SetLoggerError> {
log::set_boxed_logger(Box::new(self))
}
}
14 changes: 14 additions & 0 deletions src/log/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mod level;
mod logger;

#[cfg(feature = "categories")]
mod mutex_logs;
#[cfg(feature = "categories")]
pub use mutex_logs::Logs;

#[cfg(not(feature = "categories"))]
mod prefixed_logs;
#[cfg(not(feature = "categories"))]
pub use prefixed_logs::Logs;

pub use logger::OsLogger;
Loading

0 comments on commit e1cd875

Please sign in to comment.