dittolive_ditto/logger.rs
1//! Use [`DittoLogger`] to control Ditto logging options.
2
3use_prelude!();
4
5use std::path::Path;
6
7use tokio::sync::oneshot;
8
9#[cfg(doc)]
10use crate::error::{CoreApiErrorKind, ErrorKind};
11use crate::{error::Result, prelude::*, utils::extension_traits::FfiResultIntoRustResult};
12
13/// Type with free associated functions ("static methods") to customize the logging behavior from
14/// Ditto and log messages with the Ditto logging infrastructure.
15pub struct DittoLogger(::never_say_never::Never);
16
17impl DittoLogger {
18 /// Enable or disable logging.
19 ///
20 /// Logs exported through [`export_to_file()`] are not affected by this
21 /// setting and will also include logs emitted while `enabled` is false.
22 ///
23 /// [`export_to_file()`]: DittoLogger::export_to_file()
24 pub fn set_logging_enabled(enabled: bool) {
25 ffi_sdk::ditto_logger_enabled(enabled)
26 }
27
28 /// Return true if logging is enabled.
29 ///
30 /// Logs exported through [`export_to_file()`] are not affected by this
31 /// setting and will also include logs emitted while `enabled` is false.
32 ///
33 /// [`export_to_file()`]: DittoLogger::export_to_file()
34 pub fn get_logging_enabled() -> bool {
35 ffi_sdk::ditto_logger_enabled_get()
36 }
37
38 /// Represent whether or not emojis should be used as the log level indicator in the logs.
39 pub fn get_emoji_log_level_headings_enabled() -> bool {
40 ffi_sdk::ditto_logger_emoji_headings_enabled_get()
41 }
42
43 /// Set whether or not emojis should be used as the log level indicator in the logs.
44 pub fn set_emoji_log_level_headings_enabled(enabled: bool) {
45 ffi_sdk::ditto_logger_emoji_headings_enabled(enabled);
46 }
47
48 /// Get the current minimum log level.
49 ///
50 /// Logs exported through [`export_to_file()`] are not affected by this
51 /// setting and include all logs at [`LogLevel::Warning`] and above.
52 ///
53 /// [`export_to_file()`]: DittoLogger::export_to_file()
54 pub fn get_minimum_log_level() -> LogLevel {
55 ffi_sdk::ditto_logger_minimum_log_level_get()
56 }
57
58 /// Set the current minimum log level.
59 ///
60 /// Logs exported through [`export_to_file()`] are not affected by this
61 /// setting and include all logs at [`LogLevel::Warning`] and above.
62 ///
63 /// [`export_to_file()`]: DittoLogger::export_to_file()
64 pub fn set_minimum_log_level(log_level: LogLevel) {
65 ffi_sdk::ditto_logger_minimum_log_level(log_level);
66 }
67}
68
69impl DittoLogger {
70 /// Not part of the public API.
71 #[doc(hidden)]
72 pub fn __log_error(msg: impl Into<String>) {
73 ::ffi_sdk::ditto_log(CLogLevel::Error, char_p::new(msg.into()).as_ref())
74 }
75}
76
77/// [`DittoLogger::export_to_file()`] API.
78impl DittoLogger {
79 fn rx_export_to_file(file_path: &Path) -> oneshot::Receiver<Result<u64>> {
80 let (tx, rx) = oneshot::channel();
81 let mut tx = Some(tx);
82 ffi_sdk::dittoffi_logger_try_export_to_file_async(
83 char_p::new(file_path.to_str().expect("path to be UTF-8")).as_ref(),
84 Box::new(move |ffi_result: ::ffi_sdk::FfiResult<u64>, _: Blocking| {
85 tx.take()
86 .expect("completion callback to be called exactly once")
87 .send(ffi_result.into_rust_result().map_err(DittoError::from))
88 .ok();
89 })
90 .into(),
91 );
92 rx
93 }
94
95 /// Exports collected logs to a compressed and JSON-encoded file on the
96 /// local file system.
97 ///
98 /// `DittoLogger` locally collects a limited amount of logs at the `LogLevel::Debug`
99 /// level and above, periodically discarding old logs. The internal logger is
100 /// always enabled and works independently of the `enabled` setting and the
101 /// configured `minimum_log_level`. Its logs can be requested and downloaded
102 /// from any peer that is active in a Ditto app using the portal's device
103 /// dashboard. This method provides an alternative way of accessing those
104 /// logs by exporting them to the local filesystem.
105 ///
106 /// The logs will be written as a gzip compressed file at the URL specified
107 /// by the `file_path` parameter. When uncompressed, the file contains one
108 /// JSON value per line with the oldest entry on the first line (JSON lines
109 /// format).
110 ///
111 /// By default, Ditto limits the amount of logs it retains on disk to 15 MB
112 /// and a maximum age of 15 days. Older logs are periodically discarded once
113 /// one of these limits is reached.
114 ///
115 /// This method currently only exports logs from the most recently created
116 /// Ditto instance, even when multiple instances are running in the same
117 /// process.
118 ///
119 /// This method currently only exports logs from the most recently created
120 /// Ditto instance when multiple instances are running in the same process.
121 ///
122 /// - **Parameter** `file_path`: the path of the file to write the logs to. The file must not
123 /// already exist, and the containing directory must exist. **It is recommended for the path
124 /// to have the `.jsonl.gz` file extension** but Ditto won't enforce it, nor correct it.
125 ///
126 /// - **Errors**: it can run into I/O errors when the file cannot be written to disk. Prevent
127 /// this by ensuring that no file exists at the provided path, all parent directories exist,
128 /// sufficient permissions are granted, and that the disk is not full.
129 ///
130 /// More precisely, this "throws" a [`DittoError`] whose [`.kind()`][DittoError::kind()] is
131 /// that of an [`ErrorKind::CoreApi`], such as:
132 ///
133 /// - [`CoreApiErrorKind::IoNotFound`]
134 /// - [`CoreApiErrorKind::IoPermissionDenied`]
135 /// - [`CoreApiErrorKind::IoAlreadyExists`]
136 /// - [`CoreApiErrorKind::IoOperationFailed`]
137 ///
138 /// - **Returns**: the number of bytes written to disk.
139 ///
140 /// # Example
141 ///
142 /// ```rust ,no_run
143 /// use dittolive_ditto::{
144 /// error::{CoreApiErrorKind, ErrorKind},
145 /// prelude::*,
146 /// };
147 /// # use eprintln as error;
148 ///
149 /// # _ = async {
150 /// let ditto: Ditto = /* construct at least one ditto instance first */
151 /// # todo!();
152 /// match DittoLogger::export_to_file("/some/path.jsonl.gz").await {
153 /// Ok(_bytes_written) => { /* … */ },
154 /// Err(err) => match err.kind() {
155 /// ErrorKind::CoreApi(CoreApiErrorKind::IoNotFound) => { /* … */ },
156 /// ErrorKind::CoreApi(CoreApiErrorKind::IoPermissionDenied) => { /* … */ },
157 /// ErrorKind::CoreApi(CoreApiErrorKind::IoAlreadyExists) => { /* … */ },
158 /// ErrorKind::CoreApi(CoreApiErrorKind::IoOperationFailed) => { /* … */ },
159 /// _ => error!("{err}"),
160 /// },
161 /// }
162 /// # };
163 /// ```
164 pub async fn export_to_file(file_path: &(impl ?Sized + AsRef<Path>)) -> Result<u64> {
165 Self::rx_export_to_file(file_path.as_ref())
166 .await
167 .expect("channel to be used by the FFI")
168 }
169
170 #[cfg(any(test, doctest))] // NOT exported yet until we settle on whether to Townhousify this API.
171 /// Convenience function around [`Self::export_to_file()`], to be used when outside
172 /// of `async`hronous contexts, by falling back to a blocking call.
173 ///
174 /// ### Panics
175 ///
176 /// This function may panic if called from within an asynchronous context.
177 pub fn blocking_export_to_file(file_path: &(impl ?Sized + AsRef<Path>)) -> Result<u64> {
178 Self::rx_export_to_file(file_path.as_ref())
179 .blocking_recv()
180 .expect("channel to be used by the FFI")
181 }
182}
183
184#[cfg(any(test, doctest))]
185#[path = "logger_tests.rs"]
186mod tests;