service_logging/
logging.rs

1use crate::time::current_time_millis;
2use async_trait::async_trait;
3use serde::Serialize;
4use serde_repr::Serialize_repr;
5use std::fmt;
6
7const LIB_USER_AGENT: &str = concat![env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")];
8
9/// Severity level
10#[derive(Clone, Debug, Serialize_repr, PartialEq, PartialOrd)]
11#[repr(u8)]
12pub enum Severity {
13    /// The most verbose level, aka Trace
14    Debug = 1,
15    /// Verbose logging
16    Verbose = 2,
17    /// Information level: warnings plus major events
18    Info = 3,
19    /// all errors and warnings, and no informational messages
20    Warning = 4,
21    /// errors only
22    Error = 5,
23    /// critical errors only
24    Critical = 6,
25}
26
27/// Logging level, alias for Severity
28pub type LogLevel = Severity;
29
30impl Default for Severity {
31    fn default() -> Self {
32        Severity::Info
33    }
34}
35
36impl std::str::FromStr for Severity {
37    type Err = String;
38    fn from_str(s: &str) -> Result<Severity, Self::Err> {
39        match s {
40            "debug" | "Debug" | "DEBUG" => Ok(Severity::Debug),
41            "verbose" | "Verbose" | "VERBOSE" => Ok(Severity::Verbose),
42            "info" | "Info" | "INFO" => Ok(Severity::Info),
43            "warning" | "Warning" | "WARNING" => Ok(Severity::Warning),
44            "error" | "Error" | "ERROR" => Ok(Severity::Error),
45            "critical" | "Critical" | "CRITICAL" => Ok(Severity::Critical),
46            _ => Err(format!("Invalid severity: {}", s)),
47        }
48    }
49}
50
51impl fmt::Display for Severity {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(
54            f,
55            "{}",
56            match self {
57                Severity::Debug => "Debug",
58                Severity::Verbose => "Verbose",
59                Severity::Info => "Info",
60                Severity::Warning => "Warning",
61                Severity::Error => "Error",
62                Severity::Critical => "Critical",
63            }
64        )
65    }
66}
67
68/// LogEntry, usually created with the [`log!`] macro.
69#[derive(Debug, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct LogEntry {
72    /// Current timestamp, milliseconds since epoch in UTC
73    pub timestamp: u64,
74    /// Severity of this entry
75    pub severity: Severity,
76    /// Text value of this entry. When created with the log! macro, this field contains
77    /// json-encoded key-value pairs, sorted by key
78    pub text: String,
79    /// Optional category string (application-defined)
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub category: Option<String>,
82    /// Optional class_name (application-defined)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub class_name: Option<String>,
85    /// Optional method_name (application-defined)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub method_name: Option<String>,
88    /// Optional thread_id (not used for wasm)
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub thread_id: Option<String>,
91}
92
93//unsafe impl Send for LogEntry {}
94
95impl fmt::Display for LogEntry {
96    // omits some fields for brevity
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(f, "{} {} {}", self.timestamp, self.severity, self.text)
99    }
100}
101
102impl Default for LogEntry {
103    fn default() -> LogEntry {
104        LogEntry {
105            timestamp: current_time_millis(),
106            severity: Severity::Debug,
107            text: String::new(),
108            category: None,
109            class_name: None,
110            method_name: None,
111            thread_id: None,
112        }
113    }
114}
115
116/// Log payload for Coralogix service
117#[derive(Serialize, Debug)]
118#[serde(rename_all = "camelCase")]
119struct CxLogMsg<'a> {
120    /// api key
121    pub private_key: &'a str,
122    /// application name - dimension field
123    pub application_name: &'a str,
124    /// subsystem name - dimension field
125    pub subsystem_name: &'a str,
126    /// log messages
127    pub log_entries: Vec<LogEntry>,
128}
129
130#[derive(Clone, Debug)]
131struct CxErr {
132    msg: String,
133}
134
135impl fmt::Display for CxErr {
136    // omits some fields for brevity
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "{}", &self.msg)
139    }
140}
141impl std::error::Error for CxErr {}
142
143/// Queue of log entries to be sent to [Logger]
144#[derive(Debug)]
145pub struct LogQueue {
146    entries: Vec<LogEntry>,
147}
148
149impl Default for LogQueue {
150    fn default() -> Self {
151        Self {
152            entries: Vec::new(),
153        }
154    }
155}
156
157impl LogQueue {
158    /// Constructs a new empty log queue
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// initialize from existing entries (useful if you want to add more with log!
164    pub fn from(entries: Vec<LogEntry>) -> Self {
165        Self { entries }
166    }
167
168    /// Returns all queued items, emptying self
169    pub fn take(&mut self) -> Vec<LogEntry> {
170        let mut ve: Vec<LogEntry> = Vec::new();
171        ve.append(&mut self.entries);
172        ve
173    }
174
175    /// Returns true if there are no items to log
176    pub fn is_empty(&self) -> bool {
177        self.entries.is_empty()
178    }
179
180    /// Removes all log entries
181    pub fn clear(&mut self) {
182        self.entries.clear();
183    }
184
185    /// Appends a log entry to the queue
186    pub fn log(&mut self, e: LogEntry) {
187        self.entries.push(e)
188    }
189}
190
191impl fmt::Display for LogQueue {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        let mut buf = String::with_capacity(256);
194        for entry in self.entries.iter() {
195            if !buf.is_empty() {
196                buf.push('\n');
197            }
198            buf.push_str(&entry.to_string());
199        }
200        write!(f, "{}", buf)
201    }
202}
203
204/// Trait for logging service that receives log messages
205#[async_trait(?Send)]
206pub trait Logger: Send {
207    /// Send entries to logger
208    async fn send(
209        &self,
210        sub: &'_ str,
211        entries: Vec<LogEntry>,
212    ) -> Result<(), Box<dyn std::error::Error>>;
213}
214
215/// Logger that drops logs
216#[doc(hidden)]
217struct BlackHoleLogger {}
218#[async_trait(?Send)]
219impl Logger for BlackHoleLogger {
220    async fn send(&self, _: &'_ str, _: Vec<LogEntry>) -> Result<(), Box<dyn std::error::Error>> {
221        Ok(())
222    }
223}
224
225#[doc(hidden)]
226/// Create a logger that doesn't log anything
227/// This can be used for Default implementations that require a Logger impl
228pub fn silent_logger() -> Box<impl Logger> {
229    Box::new(BlackHoleLogger {})
230}
231
232/// Configuration parameters for Coralogix service
233#[derive(Debug)]
234pub struct CoralogixConfig<'config> {
235    /// API key, provided by Coralogix
236    pub api_key: &'config str,
237    /// Application name, included as a feature for all log messages
238    pub application_name: &'config str,
239    /// URL prefix for service invocation, e.g. `https://api.coralogix.con/api/v1/logs`
240    pub endpoint: &'config str,
241}
242
243/// Implementation of Logger for [Coralogix](https://coralogix.com/)
244#[derive(Debug)]
245pub struct CoralogixLogger {
246    api_key: String,
247    application_name: String,
248    endpoint: String,
249    client: reqwest::Client,
250}
251
252impl CoralogixLogger {
253    /// Initialize logger with configuration
254    pub fn init(config: CoralogixConfig) -> Result<Box<dyn Logger + Send>, reqwest::Error> {
255        use reqwest::header::{self, HeaderValue, CONTENT_TYPE, USER_AGENT};
256        let mut headers = header::HeaderMap::new();
257        // all our requests are json. this header is recommended by Coralogix
258        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
259        // just in case this helps us drop connection more quickly
260        //headers.insert(CONNECTION, HeaderValue::from_static("close"));
261        headers.insert(USER_AGENT, HeaderValue::from_static(LIB_USER_AGENT));
262
263        let client = reqwest::Client::builder()
264            .default_headers(headers)
265            .build()?;
266        Ok(Box::new(Self {
267            api_key: config.api_key.to_string(),
268            application_name: config.application_name.to_string(),
269            endpoint: config.endpoint.to_string(),
270            client,
271        }))
272    }
273}
274
275#[async_trait(?Send)]
276impl Logger for CoralogixLogger {
277    /// Send logs to [Coralogix](https://coralogix.com/) service.
278    /// May return error if there was a problem sending.
279    async fn send(
280        &self,
281        sub: &'_ str,
282        entries: Vec<LogEntry>,
283    ) -> Result<(), Box<dyn std::error::Error>> {
284        if !entries.is_empty() {
285            let msg = CxLogMsg {
286                subsystem_name: sub,
287                log_entries: entries,
288                private_key: &self.api_key,
289                application_name: &self.application_name,
290            };
291            let resp = self
292                .client
293                .post(&self.endpoint)
294                .json(&msg)
295                .send()
296                .await
297                .map_err(|e| CxErr { msg: e.to_string() })?;
298            check_status(resp)
299                .await
300                .map_err(|e| CxErr { msg: e.to_string() })?;
301        }
302        Ok(())
303    }
304}
305
306/// Logger that sends all messages (on wasm32 targets) to
307/// [console.log](https://developer.mozilla.org/en-US/docs/Web/API/Console/log).
308/// On Cloudflare workers, console.log output is
309/// available in the terminal for `wrangler dev` and `wrangler preview` modes.
310/// To simplify debugging and testing, ConsoleLogger on non-wasm32 targets is implemented
311/// to send output to stdout using println!
312#[derive(Default, Debug)]
313pub struct ConsoleLogger {}
314
315impl ConsoleLogger {
316    /// Initialize console logger
317    pub fn init() -> Box<dyn Logger + Send> {
318        Box::new(ConsoleLogger::default())
319    }
320}
321
322#[cfg(target_arch = "wasm32")]
323#[async_trait(?Send)]
324impl Logger for ConsoleLogger {
325    /// Sends logs to console.log handler
326    async fn send(
327        &self,
328        sub: &'_ str,
329        entries: Vec<LogEntry>,
330    ) -> Result<(), Box<dyn std::error::Error>> {
331        for e in entries.iter() {
332            let msg = format!("{} {} {} {}", e.timestamp, sub, e.severity, e.text);
333            web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&msg));
334        }
335        Ok(())
336    }
337}
338
339/// ConsoleLogger on non-wasm32 builds outputs with println!, to support debugging and testing
340#[cfg(not(target_arch = "wasm32"))]
341#[async_trait(?Send)]
342impl Logger for ConsoleLogger {
343    /// Sends logs to console.log handler
344    async fn send(
345        &self,
346        sub: &'_ str,
347        entries: Vec<LogEntry>,
348    ) -> Result<(), Box<dyn std::error::Error>> {
349        for e in entries.iter() {
350            println!("{} {} {} {}", e.timestamp, sub, e.severity, e.text);
351        }
352        Ok(())
353    }
354}
355
356// Error handling for Coralogix
357// Instead of just returning error for non-2xx status (via resp.error_for_status)
358// include response body which may have additional diagnostic info
359async fn check_status(resp: reqwest::Response) -> Result<(), Box<dyn std::error::Error>> {
360    let status = resp.status().as_u16();
361    if (200..300).contains(&status) {
362        Ok(())
363    } else {
364        let body = resp.text().await.unwrap_or_default();
365        Err(Box::new(Error::Cx(format!(
366            "Logging Error: status:{} {}",
367            status, body
368        ))))
369    }
370}
371
372#[derive(Debug)]
373enum Error {
374    // Error sending coralogix logs
375    Cx(String),
376}
377
378impl std::fmt::Display for Error {
379    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
380        write!(
381            f,
382            "{}",
383            match self {
384                Error::Cx(s) => s,
385            }
386        )
387    }
388}
389
390impl std::error::Error for Error {}