├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── LICENSE-MIT ├── README.md ├── src ├── lib.rs ├── logging.rs └── time.rs └── tests ├── console_log.rs └── fake_console_log.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | /target 3 | pkg/ 4 | .idea 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.4.7 4 | 5 | - set user agent on coralogix calls to crate name & version 6 | - don't set Connection:close 7 | 8 | ## v0.4.6 9 | 10 | - Coralogix config parameters can be &str and don't need to &'static str 11 | 12 | ## v0.4.5 2021-01-23 13 | - updated dependency to reqwest 0.11 14 | 15 | ## v0.4.2 2021-01-12 16 | - added silent_logger (logs nothing) 17 | 18 | ## v0.4.0 2020-12-31 19 | 20 | - Breaking change: 21 | - Removed prelude module. If you previously imported "service_logging::prelude::*", 22 | replace it with "service_logging::Logger" to import the trait. 23 | 24 | - New features 25 | 26 | - added implementation of ConsoleLogger for non-wasm builds, 27 | which sends output to stdout using println!. 28 | The most likely use for this is testing. 29 | 30 | 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "service-logging" 3 | version = "0.4.7" 4 | authors = ["stevelr "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | description = "Asynchronous structured logging in tiny library (6KB) with clients for Coralogix and console.log. WASM compatible" 8 | repository = "https://github.com/stevelr/service-logging" 9 | homepage = "https://github.com/stevelr/service-logging" 10 | documentation = "https://docs.rs/service-logging" 11 | readme = "README.md" 12 | keywords = ["logging","wasm","coralogix"] 13 | categories = ["development-tools::debugging","api-bindings","wasm","asynchronous"] 14 | 15 | [features] 16 | # "std": use std allocator; "alloc": you provide allocator 17 | default=["alloc"] 18 | std = ["serde_json/std", "serde/std" ] 19 | alloc = ["serde_json/alloc", "serde/alloc" ] 20 | 21 | [dependencies] 22 | async-trait = "0.1" 23 | serde_repr = "0.1" 24 | reqwest = { version="0.11", features=["json"] } 25 | 26 | # optional 27 | serde_json = { version="1.0", default-features=false, optional=true } 28 | serde = { version = "1.0", optional=true, features=["derive"] } 29 | 30 | [target.'cfg(target_arch = "wasm32")'.dependencies] 31 | js-sys = "0.3" 32 | wasm-bindgen = "0.2" 33 | web-sys = { version="0.3", features=["console"] } 34 | 35 | [dev-dependencies] 36 | wasm-bindgen-test = "0.3" 37 | wasm-bindgen-futures = "0.4" 38 | tokio = { version="1.0", features=["macros","rt"] } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2020-2021 service-logging developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Asynchronous structured logging in tiny library (6KB) with clients for Coralogix and console.log. WASM compatible. 2 | 3 | ## Usage 4 | 5 | Use the `log!` macro to log key-value pairs, which are json-encoded 6 | before sending to logging service 7 | 8 | ```rust 9 | use service_logging::{log, LogQueue, Severity::{Info,Error}}; 10 | let logger = CoralogixLogger::init(CoralogixConfig{ 11 | api_key: "0000", 12 | application_name: "MyApp", 13 | endpoint: "https://api.coralogix.com/api/v1/logs"}); 14 | let mut lq = LogQueue::default(); 15 | 16 | log!(lq, Info, 17 | method: "GET", 18 | url: url, 19 | status: 200 20 | ); 21 | 22 | log!(lq, Error, 23 | user: user, 24 | message: "Too many failed login attempts", 25 | attempts: count 26 | ); 27 | 28 | logger.send("http", lq.take()).await?; 29 | ``` 30 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! Library for aggregating logs and sending to logging service. 3 | //! Contains implementations for [Coralogix](https://coralogix.com/) 4 | //! and (for wasm) console.log 5 | mod logging; 6 | mod time; 7 | 8 | /// ConsoleLogger sends output to the javascript console (wasm32 targets) or stdout (println! for 9 | /// non-wasm32 targets) 10 | pub use logging::ConsoleLogger; 11 | pub use logging::{ 12 | silent_logger, CoralogixConfig, CoralogixLogger, LogEntry, LogLevel, LogQueue, Logger, Severity, 13 | }; 14 | 15 | /// The `log!` macro can be used to create structured log entries for later use by [Logger.send](Logger::send) 16 | /// The first two parameters are fixed: 17 | /// - a writable queue (or something with a log() method) 18 | /// - severity level 19 | /// All remaining parameters are in the form key:value. Key is any word (using the same syntax 20 | /// as 21 | /// 22 | /// ``` 23 | /// use service_logging::{log, LogQueue, Severity::Info}; 24 | /// let mut lq = LogQueue::default(); 25 | /// 26 | /// // log http parameters 27 | /// log!(lq, Info, method: "GET", url: "https://example.com", status: 200); 28 | /// ``` 29 | /// 30 | /// Parameters are of the form: (queue, severity, key:value, key:value, ...). 31 | /// `queue` is any object that implements `fn add(&mut self, e: [LogEntry])` 32 | /// (such as [LogQueue] or [Context](https://docs.rs/wasm-service/0.2/wasm_service/struct.Context.html)) 33 | /// 34 | /// Values can be anything that implements [ToString] 35 | /// Key names must use the same syntax as a rust identifier, e.g., no spaces, punctuation, etc. 36 | /// 37 | /// The following keys are "special" (known to Coralogix and used for categorization 38 | /// in the coralogix dashboard): `text`, `category`, `class_name`, `method_name`, `thread_id` 39 | /// If `text` is not defined, all non-coralogix keys are converted into a json string and 40 | /// passed as the value of 'text'. (If `text` is also defined, any non-coralogix keys will be 41 | /// silently dropped). 42 | #[macro_export] 43 | macro_rules! log { 44 | ( $queue:expr, $sev:expr, $( $key:tt $_t:tt $val:expr ),* ) => {{ 45 | let mut fields: std::collections::BTreeMap = std::collections::BTreeMap::new(); 46 | let mut has_text = false; 47 | let mut entry = service_logging::LogEntry { severity: ($sev), ..Default::default() }; 48 | $( 49 | let val = $val.to_string(); 50 | let key = stringify!($key); 51 | match key { 52 | "text" => { entry.text = val; has_text = true; }, 53 | "category" => { entry.category = Some(val); }, 54 | "class_name" => { entry.class_name = Some(val); }, 55 | "method_name" => { entry.method_name = Some(val); }, 56 | "thread_id" => { entry.thread_id = Some(val); }, 57 | _ => { fields.insert(key.to_string(), val); } 58 | } 59 | )* 60 | if !has_text { 61 | entry.text = match serde_json::to_string(&fields) { 62 | Ok(s) => s, 63 | Err(e) => format!("error serializing message: {}",e), 64 | }; 65 | } 66 | $queue.log(entry); 67 | }}; 68 | } 69 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use crate::time::current_time_millis; 2 | use async_trait::async_trait; 3 | use serde::Serialize; 4 | use serde_repr::Serialize_repr; 5 | use std::fmt; 6 | 7 | const 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)] 12 | pub 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 28 | pub type LogLevel = Severity; 29 | 30 | impl Default for Severity { 31 | fn default() -> Self { 32 | Severity::Info 33 | } 34 | } 35 | 36 | impl std::str::FromStr for Severity { 37 | type Err = String; 38 | fn from_str(s: &str) -> Result { 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 | 51 | impl 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")] 71 | pub 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, 82 | /// Optional class_name (application-defined) 83 | #[serde(skip_serializing_if = "Option::is_none")] 84 | pub class_name: Option, 85 | /// Optional method_name (application-defined) 86 | #[serde(skip_serializing_if = "Option::is_none")] 87 | pub method_name: Option, 88 | /// Optional thread_id (not used for wasm) 89 | #[serde(skip_serializing_if = "Option::is_none")] 90 | pub thread_id: Option, 91 | } 92 | 93 | //unsafe impl Send for LogEntry {} 94 | 95 | impl 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 | 102 | impl 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")] 119 | struct 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, 128 | } 129 | 130 | #[derive(Clone, Debug)] 131 | struct CxErr { 132 | msg: String, 133 | } 134 | 135 | impl 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 | } 141 | impl std::error::Error for CxErr {} 142 | 143 | /// Queue of log entries to be sent to [Logger] 144 | #[derive(Debug)] 145 | pub struct LogQueue { 146 | entries: Vec, 147 | } 148 | 149 | impl Default for LogQueue { 150 | fn default() -> Self { 151 | Self { 152 | entries: Vec::new(), 153 | } 154 | } 155 | } 156 | 157 | impl 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) -> Self { 165 | Self { entries } 166 | } 167 | 168 | /// Returns all queued items, emptying self 169 | pub fn take(&mut self) -> Vec { 170 | let mut ve: Vec = 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 | 191 | impl 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)] 206 | pub trait Logger: Send { 207 | /// Send entries to logger 208 | async fn send( 209 | &self, 210 | sub: &'_ str, 211 | entries: Vec, 212 | ) -> Result<(), Box>; 213 | } 214 | 215 | /// Logger that drops logs 216 | #[doc(hidden)] 217 | struct BlackHoleLogger {} 218 | #[async_trait(?Send)] 219 | impl Logger for BlackHoleLogger { 220 | async fn send(&self, _: &'_ str, _: Vec) -> Result<(), Box> { 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 228 | pub fn silent_logger() -> Box { 229 | Box::new(BlackHoleLogger {}) 230 | } 231 | 232 | /// Configuration parameters for Coralogix service 233 | #[derive(Debug)] 234 | pub 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)] 245 | pub struct CoralogixLogger { 246 | api_key: String, 247 | application_name: String, 248 | endpoint: String, 249 | client: reqwest::Client, 250 | } 251 | 252 | impl CoralogixLogger { 253 | /// Initialize logger with configuration 254 | pub fn init(config: CoralogixConfig) -> Result, 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)] 276 | impl 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, 283 | ) -> Result<(), Box> { 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)] 313 | pub struct ConsoleLogger {} 314 | 315 | impl ConsoleLogger { 316 | /// Initialize console logger 317 | pub fn init() -> Box { 318 | Box::new(ConsoleLogger::default()) 319 | } 320 | } 321 | 322 | #[cfg(target_arch = "wasm32")] 323 | #[async_trait(?Send)] 324 | impl Logger for ConsoleLogger { 325 | /// Sends logs to console.log handler 326 | async fn send( 327 | &self, 328 | sub: &'_ str, 329 | entries: Vec, 330 | ) -> Result<(), Box> { 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)] 342 | impl Logger for ConsoleLogger { 343 | /// Sends logs to console.log handler 344 | async fn send( 345 | &self, 346 | sub: &'_ str, 347 | entries: Vec, 348 | ) -> Result<(), Box> { 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 359 | async fn check_status(resp: reqwest::Response) -> Result<(), Box> { 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)] 373 | enum Error { 374 | // Error sending coralogix logs 375 | Cx(String), 376 | } 377 | 378 | impl 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 | 390 | impl std::error::Error for Error {} 391 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | /// Returns current time in UTC, as integer milliseconds since EPOCH 2 | #[cfg(target_arch = "wasm32")] 3 | // logging api supports floating pt timestamps for fractional millis, 4 | // but we don't need that resolution, and serde float support is bulky 5 | pub fn current_time_millis() -> u64 { 6 | js_sys::Date::now() as u64 7 | } 8 | 9 | /// Returns current time in UTC, as integer milliseconds since EPOCH 10 | #[cfg(not(target_arch = "wasm32"))] 11 | pub fn current_time_millis() -> u64 { 12 | use std::time::SystemTime; 13 | match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 14 | Ok(n) => n.as_millis() as u64, 15 | Err(_) => 0, // panic!("SystemTime before UNIX EPOCH!"), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/console_log.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 2 | // using ConsoleLogger to write to javascript/browser console.log 3 | 4 | use service_logging::{log, ConsoleLogger, LogQueue, Severity}; 5 | 6 | use wasm_bindgen_test::*; 7 | 8 | #[wasm_bindgen_test] 9 | async fn test_console_log() { 10 | let mut log_queue = LogQueue::default(); 11 | log!(log_queue, Severity::Info, one:"Thing One", two: "Thing Two"); 12 | let logger = ConsoleLogger::init(); 13 | logger 14 | .send("test_console_log", log_queue.take()) 15 | .await 16 | .expect("send"); 17 | } 18 | -------------------------------------------------------------------------------- /tests/fake_console_log.rs: -------------------------------------------------------------------------------- 1 | // Example use of ConsoleLogger in non-wasm32 builds. 2 | // 3 | #[cfg(not(target_arch = "wasm32"))] 4 | use service_logging::{log, ConsoleLogger, LogQueue, Severity}; 5 | 6 | #[cfg(not(target_arch = "wasm32"))] 7 | #[tokio::test] 8 | async fn test_console_log() { 9 | let mut log_queue = LogQueue::default(); 10 | log!(log_queue, Severity::Info, one:"Thing One", two: "Thing Two"); 11 | let logger = ConsoleLogger::init(); 12 | logger 13 | .send("test_console_log", log_queue.take()) 14 | .await 15 | .expect("send"); 16 | } 17 | --------------------------------------------------------------------------------