dittolive-ditto 5.0.0

Ditto is a peer to peer cross-platform database that allows mobile, web, IoT and server apps to sync with or without an internet connection.
Documentation
//! Use [`DittoLogger`] to control Ditto logging options.

use std::{
    path::Path,
    sync::{Arc, Mutex},
};

use safer_ffi::{closure::boxed::*, prelude::*};
use tokio::sync::oneshot;

#[cfg(doc)]
use crate::error::{CoreApiErrorKind, ErrorKind};
use crate::{error::Result, prelude::*, utils::extension_traits::FfiResultIntoRustResult};

type StoredCallback = Arc<dyn Fn(LogLevel, &str) + Send + Sync>;
static CUSTOM_LOG_CALLBACK: Mutex<Option<StoredCallback>> = Mutex::new(None);

/// FFI shim that bridges C callbacks to Rust closures.
extern "C" fn custom_log_callback_shim(level: CLogLevel, msg: char_p::Box) {
    // Clone the Arc (if present) while holding the lock, then invoke outside
    // the lock. Use .ok() to silently skip if poisoned - crashing in a logging
    // callback would be worse than missing logs.
    let callback = CUSTOM_LOG_CALLBACK
        .lock()
        .ok()
        .and_then(|guard| guard.as_ref().map(Arc::clone));

    if let Some(cb) = callback {
        cb(level, msg.to_str());
    }
}

/// Type with free associated functions ("static methods") to customize the logging behavior from
/// Ditto and log messages with the Ditto logging infrastructure.
pub struct DittoLogger(::never_say_never::Never);

impl DittoLogger {
    /// Enable or disable logging.
    ///
    /// Logs exported through [`export_to_file()`] are not affected by this
    /// setting and will also include logs emitted while `enabled` is false.
    ///
    /// [`export_to_file()`]: DittoLogger::export_to_file()
    pub fn set_logging_enabled(enabled: bool) {
        ffi_sdk::ditto_logger_enabled(enabled)
    }

    /// Return true if logging is enabled.
    ///
    /// Logs exported through [`export_to_file()`] are not affected by this
    /// setting and will also include logs emitted while `enabled` is false.
    ///
    /// [`export_to_file()`]: DittoLogger::export_to_file()
    pub fn get_logging_enabled() -> bool {
        ffi_sdk::ditto_logger_enabled_get()
    }

    /// Get the current minimum log level.
    ///
    /// Logs exported through [`export_to_file()`] are not affected by this
    /// setting and include all logs at [`LogLevel::Debug`] and above.
    ///
    /// [`export_to_file()`]: DittoLogger::export_to_file()
    pub fn get_minimum_log_level() -> LogLevel {
        ffi_sdk::ditto_logger_minimum_log_level_get()
    }

    /// Set the current minimum log level.
    ///
    /// Logs exported through [`export_to_file()`] are not affected by this
    /// setting and include all logs at [`LogLevel::Debug`] and above.
    ///
    /// [`export_to_file()`]: DittoLogger::export_to_file()
    pub fn set_minimum_log_level(log_level: LogLevel) {
        ffi_sdk::ditto_logger_minimum_log_level(log_level);
    }
}

impl DittoLogger {
    /// Not part of the public API.
    #[doc(hidden)]
    pub fn __log_error(msg: impl Into<String>) {
        ::ffi_sdk::ditto_log(CLogLevel::Error, char_p::new(msg.into()).as_ref())
    }
}

/// Custom log callback API.
impl DittoLogger {
    /// Registers a custom callback to receive Ditto log messages.
    ///
    /// When set, the callback is invoked for each log message that Ditto emits.
    ///
    /// **Panics:** The callback must not panic. If it does, the process will abort.
    ///
    /// **Re-entrancy:** The callback should avoid calling Ditto APIs that emit
    /// logs, as this causes recursion.
    ///
    /// # Example
    ///
    /// ```rust
    /// use dittolive_ditto::prelude::*;
    ///
    /// // Route Ditto logs to the `log` crate
    /// DittoLogger::set_custom_log_callback(|level, message| match level {
    ///     LogLevel::Error => log::error!("{}", message),
    ///     LogLevel::Warning => log::warn!("{}", message),
    ///     LogLevel::Info => log::info!("{}", message),
    ///     LogLevel::Debug => log::debug!("{}", message),
    ///     LogLevel::Verbose => log::trace!("{}", message),
    /// });
    ///
    /// // Later, remove the callback
    /// DittoLogger::clear_custom_log_callback();
    /// ```
    pub fn set_custom_log_callback<F>(callback: F)
    where
        F: Fn(LogLevel, &str) + Send + Sync + 'static,
    {
        let mut guard = CUSTOM_LOG_CALLBACK.lock().unwrap();
        let was_none = guard.is_none();
        *guard = Some(Arc::new(callback) as StoredCallback);
        if was_none {
            ffi_sdk::ditto_logger_set_custom_log_cb(Some(ffi_sdk::CustomLogCb(
                custom_log_callback_shim,
            )));
        }
    }

    /// Removes any registered custom log callback.
    ///
    /// After calling this method, log messages will no longer be forwarded to
    /// the previously registered callback.
    pub fn clear_custom_log_callback() {
        let mut guard = CUSTOM_LOG_CALLBACK.lock().unwrap();
        ffi_sdk::ditto_logger_set_custom_log_cb(None);
        *guard = None;
    }
}

/// [`DittoLogger::export_to_file()`] API.
impl DittoLogger {
    fn rx_export_to_file(file_path: &Path) -> oneshot::Receiver<Result<u64>> {
        let (tx, rx) = oneshot::channel();
        let mut tx = Some(tx);
        ffi_sdk::dittoffi_logger_try_export_to_file_async(
            char_p::new(file_path.to_str().expect("path to be UTF-8")).as_ref(),
            #[allow(clippy::useless_conversion)] // False positive (AFAICT)
            BoxDynFnMut1::new(Box::new(move |ffi_result: ::ffi_sdk::FfiResult<u64>| {
                tx.take()
                    .expect("completion callback to be called exactly once")
                    .send(ffi_result.into_rust_result().map_err(DittoError::from))
                    .ok();
            }))
            .into(),
        );
        rx
    }

    /// Exports collected logs to a compressed and JSON-encoded file on the
    /// local file system.
    ///
    /// `DittoLogger` locally collects a limited amount of logs at the `LogLevel::Debug`
    /// level and above, periodically discarding old logs. The internal logger is
    /// always enabled and works independently of the `enabled` setting and the
    /// configured `minimum_log_level`. Its logs can be requested and downloaded
    /// from any peer that is active in a Ditto app using the portal's device
    /// dashboard. This method provides an alternative way of accessing those
    /// logs by exporting them to the local filesystem.
    ///
    /// The logs will be written as a gzip-compressed file at the path specified
    /// by the `file_path` parameter. When uncompressed, the file contains one
    /// JSON value per line with the oldest entry on the first line (JSON lines
    /// format).
    ///
    /// By default, Ditto limits the amount of logs it retains on disk to 15 MB
    /// and a maximum age of 15 days. Older logs are periodically discarded once
    /// one of these limits is reached.
    ///
    /// This method currently only exports logs from the most recently created
    /// Ditto instance, even when multiple instances are running in the same
    /// process.
    ///
    /// - **Parameter** `file_path`: the path of the file to write the logs to. The file must not
    ///   already exist, and the containing directory must exist. **It is recommended for the path
    ///   to have the `.jsonl.gz` file extension** but Ditto won't enforce it, nor correct it.
    ///
    /// - **Errors**: it can run into I/O errors when the file cannot be written to disk. Prevent
    ///   this by ensuring that no file exists at the provided path, all parent directories exist,
    ///   sufficient permissions are granted, and that the disk is not full.
    ///
    ///   More precisely, this "throws" a [`DittoError`] whose [`.kind()`][DittoError::kind()] is
    ///   that of an [`ErrorKind::CoreApi`], such as:
    ///
    ///     - [`CoreApiErrorKind::IoNotFound`]
    ///     - [`CoreApiErrorKind::IoPermissionDenied`]
    ///     - [`CoreApiErrorKind::IoAlreadyExists`]
    ///     - [`CoreApiErrorKind::IoOperationFailed`]
    ///
    /// - **Returns**: the number of bytes written to disk.
    ///
    /// # Example
    ///
    /// ```
    /// use dittolive_ditto::{
    ///     error::{CoreApiErrorKind, ErrorKind},
    ///     prelude::*,
    /// };
    /// # use eprintln as error;
    /// # #[tokio::main]
    /// # async fn main() {
    /// # let (_root, _ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
    /// # let export_path = std::env::temp_dir().join(format!("ditto-export-{}.jsonl.gz", std::process::id()));
    /// # let export_path = export_path.to_str().unwrap();
    ///
    /// // At least one Ditto instance must exist before calling export_to_file.
    /// match DittoLogger::export_to_file(export_path).await {
    ///     Ok(_bytes_written) => { /* … */ }
    ///     Err(err) => match err.kind() {
    ///         ErrorKind::CoreApi(CoreApiErrorKind::IoNotFound) => { /* … */ }
    ///         ErrorKind::CoreApi(CoreApiErrorKind::IoPermissionDenied) => { /* … */ }
    ///         ErrorKind::CoreApi(CoreApiErrorKind::IoAlreadyExists) => { /* … */ }
    ///         ErrorKind::CoreApi(CoreApiErrorKind::IoOperationFailed) => { /* … */ }
    ///         _ => error!("{err}"),
    ///     },
    /// }
    /// # }
    /// ```
    pub async fn export_to_file(file_path: &(impl ?Sized + AsRef<Path>)) -> Result<u64> {
        Self::rx_export_to_file(file_path.as_ref())
            .await
            .expect("channel to be used by the FFI")
    }

    #[cfg(any(test, doctest))] // NOT exported yet until we settle on whether to Townhousify this API.
    /// Convenience function around [`Self::export_to_file()`], to be used when outside
    /// of `async`hronous contexts, by falling back to a blocking call.
    ///
    /// ### Panics
    ///
    /// This function may panic if called from within an asynchronous context.
    pub fn blocking_export_to_file(file_path: &(impl ?Sized + AsRef<Path>)) -> Result<u64> {
        Self::rx_export_to_file(file_path.as_ref())
            .blocking_recv()
            .expect("channel to be used by the FFI")
    }
}

#[cfg(any(test, doctest))]
#[path = "logger_tests.rs"]
mod tests;