diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe1479c..c216c90 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,7 +5,6 @@ on: branches: - main - develop - - feature/* pull_request: jobs: diff --git a/Cargo.lock b/Cargo.lock index 239f1ae..863b445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstyle" version = "1.0.11" @@ -50,12 +65,47 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "difflib" version = "0.4.0" @@ -77,6 +127,46 @@ dependencies = [ "num-traits", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -245,6 +335,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.219" @@ -265,6 +367,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -274,6 +388,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "smallvec" version = "1.15.1" @@ -308,10 +428,12 @@ dependencies = [ [[package]] name = "tincre-logger" -version = "0.1.0" +version = "0.1.1" dependencies = [ "assert_cmd", + "chrono", "predicates", + "serde_json", "tracing", "tracing-subscriber", ] @@ -398,6 +520,64 @@ dependencies = [ "libc", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -419,3 +599,62 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index 3940010..a6fb107 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,8 @@ exclude = [ ] [dependencies] +chrono = "0.4.41" +serde_json = "1.0.141" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } diff --git a/examples/simple.rs b/examples/simple.rs index f60b949..e2695f5 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,10 +1,26 @@ // examples/simple.rs - +use serde_json::json; use tincre_logger::logger; fn main() { + // Simple log messages logger::log("hello from the example"); logger::warn("this is a warning"); logger::error("this is an error"); logger::debug("this is a debug message"); + + // Structured log messages with additional metadata + logger::info_with( + "user signed in", + json!({ "user_id": 42, "method": "oauth" }), + ); + logger::warn_with("cache miss", json!({ "key": "homepage", "misses": 3 })); + logger::error_with( + "db failure", + json!({ "code": 500, "query": "SELECT * FROM users" }), + ); + logger::debug_with( + "loaded config", + json!({ "env": "local", "debug_mode": true }), + ); } diff --git a/src/logger.rs b/src/logger.rs index dc27332..8187ef6 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,5 +1,3 @@ -// src/logger.rs - //! A simple, "zero-setup" logger that works out-of-the-box. //! //! This module provides a set of functions for logging messages at different @@ -18,10 +16,12 @@ //! // To see debug messages, run with `RUST_LOG=debug` //! logger::debug("User 'admin' logged in."); //! } -//! ``` +use chrono::Utc; +use serde_json::Value; use tracing::{debug, error, info, warn}; +// --- Setup --- #[cfg_attr(coverage, coverage(off))] #[inline(always)] fn ensure_initialized() { @@ -42,6 +42,7 @@ fn ensure_initialized() { }); } } + // --- Public API Functions --- /// Logs a message at the `INFO` level. This is an alias for `info()`. @@ -118,9 +119,92 @@ pub fn debug(message: &str) { debug!(message); } +/// Logs a message at the `INFO` level with additional structured metadata. +/// +/// This function accepts a message and a second parameter representing structured +/// data. The data is serialized and logged as part of the event. A UTC timestamp is +/// automatically injected. +/// +/// # Example +/// +/// ``` +/// use tincre_logger::logger; +/// use serde_json::json; +/// +/// logger::info_with("User signed in", json!({ "user_id": 42, "method": "oauth" })); +/// ``` +pub fn info_with(message: &str, data: impl Into) { + ensure_initialized(); + let timestamp = Utc::now().to_rfc3339(); + info!(%timestamp, message = %message, data = ?data.into()); +} + +/// Logs a message at the `WARN` level with additional structured metadata. +/// +/// This function is useful for highlighting warnings while attaching extra +/// information, such as rate limit states or configuration drift. A UTC timestamp +/// is automatically injected. +/// +/// # Example +/// +/// ``` +/// use tincre_logger::logger; +/// use serde_json::json; +/// +/// logger::warn_with("Cache miss", json!({ "key": "homepage", "attempts": 2 })); +/// ``` +pub fn warn_with(message: &str, data: impl Into) { + ensure_initialized(); + let timestamp = Utc::now().to_rfc3339(); + warn!(%timestamp, message = %message, data = ?data.into()); +} + +/// Logs a message at the `ERROR` level with additional structured metadata. +/// +/// This function is intended for errors that should be captured in monitoring +/// pipelines with relevant context, such as error codes or service names. +/// A UTC timestamp is automatically injected. +/// +/// # Example +/// +/// ``` +/// use tincre_logger::logger; +/// use serde_json::json; +/// +/// logger::error_with("Database write failed", json!({ "table": "users", "code": 500 })); +/// ``` +pub fn error_with(message: &str, data: impl Into) { + ensure_initialized(); + let timestamp = Utc::now().to_rfc3339(); + error!(%timestamp, message = %message, data = ?data.into()); +} + +/// Logs a message at the `DEBUG` level with additional structured metadata. +/// +/// By default, debug messages are hidden. They can be enabled by setting +/// the `RUST_LOG` environment variable (e.g., `RUST_LOG=debug`). A UTC timestamp +/// is automatically injected. +/// +/// # Example +/// +/// ``` +/// use tincre_logger::logger; +/// use serde_json::json; +/// +/// // To see this message, run your application with `RUST_LOG=debug` +/// logger::debug_with("Loaded config", json!({ "env": "dev", "debug_mode": true })); +/// ``` +pub fn debug_with(message: &str, data: impl Into) { + ensure_initialized(); + let timestamp = Utc::now().to_rfc3339(); + debug!(%timestamp, message = %message, data = ?data.into()); +} + +// --- Unit Tests --- #[cfg(test)] mod tests { use super::*; + use serde_json::json; use std::io; use std::sync::{Arc, Mutex}; use tracing_subscriber::{filter::LevelFilter, fmt, layer::SubscriberExt, registry}; @@ -187,4 +271,34 @@ mod tests { assert!(output.contains("ERROR") && output.contains("an error message")); assert!(output.contains("DEBUG") && output.contains("a debug message")); } + + #[test] + fn it_logs_all_levels_with_data() { + let writer = TestWriter::new(); + let writer_clone = writer.clone(); + + let subscriber = registry() + .with( + fmt::layer() + .with_writer(move || writer_clone.clone()) + .with_ansi(false), + ) + .with(LevelFilter::TRACE); + + tracing::subscriber::with_default(subscriber, || { + info_with("structured info", json!({ "k": "v" })); + warn_with("structured warn", json!({ "warn_level": 2 })); + error_with("structured error", json!({ "err": "boom" })); + debug_with("structured debug", json!({ "flag": true })); + }); + + let output = writer.get_contents(); + + assert!(output.contains("INFO") && output.contains("structured info")); + assert!(output.contains("WARN") && output.contains("structured warn")); + assert!(output.contains("ERROR") && output.contains("structured error")); + assert!(output.contains("DEBUG") && output.contains("structured debug")); + assert!(output.contains("timestamp")); // check for injected field + assert!(output.contains('k') && output.contains('v')); + } } diff --git a/tests/integration.rs b/tests/integration.rs index 0c6a3b4..98517ca 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -3,7 +3,6 @@ use predicates::prelude::*; #[test] fn test_example_output_default_level() { - // This is the canonical way to run an example and test its output. let mut cmd = Command::new("cargo"); cmd.args(["run", "--example", "simple", "--quiet"]); @@ -11,6 +10,10 @@ fn test_example_output_default_level() { .stdout(predicate::str::contains("hello from the example")) .stdout(predicate::str::contains("this is a warning")) .stdout(predicate::str::contains("this is an error")) + .stdout(predicate::str::contains("user signed in")) + .stdout(predicate::str::contains("cache miss")) + .stdout(predicate::str::contains("db failure")) + .stdout(predicate::str::contains("loaded config").not()) // debug only .stdout(predicate::str::contains("this is a debug message").not()) .success(); } @@ -23,6 +26,7 @@ fn test_example_output_debug_level() { cmd.assert() .stdout(predicate::str::contains("this is a debug message")) + .stdout(predicate::str::contains("loaded config")) .success(); }