use wide_log::wide_log;
wide_log!({
"service": {
"name": null,
"version": "1.0.0",
},
"requests": counter!,
"status": null,
});
use sonic_rs::{JsonContainerTrait, JsonValueTrait};
use std::sync::{Arc, Mutex};
type CaptureSlot = Arc<Mutex<Option<String>>>;
#[allow(clippy::type_complexity)]
fn capture() -> (
CaptureSlot,
impl FnOnce(&wide_log::WideEvent<EventKey>) + Send + 'static,
) {
let slot: CaptureSlot = Arc::new(Mutex::new(None));
let slot_clone = slot.clone();
let emit = move |we: &wide_log::WideEvent<EventKey>| {
*slot_clone.lock().unwrap() = Some(we.to_json().unwrap());
};
(slot, emit)
}
#[tokio::test]
async fn scope_default_works() {
let (slot, emit) = capture();
let result = scope(emit, async {
wl_set!("service.name", "async-svc");
wl_inc!("requests");
info!("async request");
42
})
.await;
assert_eq!(result, 42);
let json = slot.lock().unwrap().clone().unwrap();
let parsed: sonic_rs::Value = sonic_rs::from_str(&json).unwrap();
assert_eq!(parsed["service"]["name"], "async-svc");
assert_eq!(parsed["service"]["version"], "1.0.0");
assert_eq!(parsed["requests"], 1);
assert!(parsed["duration"]["total_ms"].is_number());
assert_eq!(parsed["log"][0]["level"], "info");
assert_eq!(parsed["log"][0]["message"], "async request");
}
#[tokio::test]
async fn scope_default_uses_default_emit() {
let result = scope_default(async {
wl_set!("status", "ok");
wl_inc!("requests");
7
})
.await;
assert_eq!(result, 7);
}
#[tokio::test]
async fn macros_work_across_await() {
let (slot, emit) = capture();
scope(emit, async {
wl_set!("status", "pending");
info!("before await");
tokio::task::yield_now().await;
info!("after await");
wl_set!("status", "done");
})
.await;
let json = slot.lock().unwrap().clone().unwrap();
let parsed: sonic_rs::Value = sonic_rs::from_str(&json).unwrap();
assert_eq!(parsed["status"], "done");
let log = &parsed["log"];
let arr = log.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["message"], "before await");
assert_eq!(arr[1]["message"], "after await");
}
#[tokio::test]
async fn nested_async_scopes() {
let outer_slot = Arc::new(Mutex::new(None));
let inner_slot = Arc::new(Mutex::new(None));
let oc = outer_slot.clone();
let ic = inner_slot.clone();
scope(
move |ev| *oc.lock().unwrap() = Some(ev.to_json().unwrap()),
async {
wl_set!("status", "outer");
wl_inc!("requests");
scope(
move |ev| *ic.lock().unwrap() = Some(ev.to_json().unwrap()),
async {
wl_set!("status", "inner");
wl_inc!("requests");
info!("inner scope");
},
)
.await;
info!("outer after inner");
},
)
.await;
let outer_json = outer_slot.lock().unwrap().clone().unwrap();
let inner_json = inner_slot.lock().unwrap().clone().unwrap();
let outer_parsed: sonic_rs::Value = sonic_rs::from_str(&outer_json).unwrap();
let inner_parsed: sonic_rs::Value = sonic_rs::from_str(&inner_json).unwrap();
assert_eq!(outer_parsed["status"], "outer");
assert_eq!(outer_parsed["requests"], 1);
assert_eq!(inner_parsed["status"], "inner");
assert_eq!(inner_parsed["requests"], 1);
let outer_log = outer_parsed["log"].as_array().unwrap();
assert_eq!(outer_log.len(), 1);
assert_eq!(outer_log[0]["message"], "outer after inner");
let inner_log = inner_parsed["log"].as_array().unwrap();
assert_eq!(inner_log.len(), 1);
assert_eq!(inner_log[0]["message"], "inner scope");
}
#[tokio::test]
async fn current_is_none_without_scope() {
assert!(current().is_none());
}
#[tokio::test]
async fn current_is_some_inside_scope() {
scope(|_| {}, async {
assert!(current().is_some());
})
.await;
}
#[tokio::test]
async fn concurrent_tasks_have_separate_events() {
let slots: Vec<Arc<Mutex<Option<String>>>> =
(0..5).map(|_| Arc::new(Mutex::new(None))).collect();
let mut handles = vec![];
for (i, slot) in slots.iter().enumerate() {
let s = slot.clone();
handles.push(tokio::spawn(scope(
move |ev| *s.lock().unwrap() = Some(ev.to_json().unwrap()),
async move {
wl_set!("service.name", format!("task-{i}"));
wl_inc!("requests");
info!("task {} running", i);
tokio::task::yield_now().await;
info!("task {} done", i);
},
)));
}
for h in handles {
h.await.unwrap();
}
for (i, slot) in slots.iter().enumerate() {
let json = slot.lock().unwrap().clone().unwrap();
let parsed: sonic_rs::Value = sonic_rs::from_str(&json).unwrap();
assert_eq!(parsed["service"]["name"], format!("task-{i}"));
assert_eq!(parsed["requests"], 1);
let log = parsed["log"].as_array().unwrap();
assert_eq!(log.len(), 2);
assert_eq!(log[0]["message"], format!("task {i} running"));
assert_eq!(log[1]["message"], format!("task {i} done"));
}
}
use std::convert::Infallible;
use wide_log::__re_exports::tower::{Layer, Service};
#[derive(Clone)]
struct OkService;
impl Service<String> for OkService {
type Response = String;
type Error = Infallible;
type Future = std::future::Ready<Result<String, Infallible>>;
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, req: String) -> Self::Future {
std::future::ready(Ok(req))
}
}
#[tokio::test]
async fn middleware_wraps_request_in_scope() {
let mut middleware = WideLogLayer.layer(OkService);
let response = middleware.call("hello".to_string()).await.unwrap();
assert_eq!(response, "hello");
assert!(current().is_none());
}
#[tokio::test]
async fn middleware_handler_can_use_macros() {
let mut middleware = WideLogLayer.layer(OkService);
let response = middleware.call("request-body".to_string()).await.unwrap();
assert_eq!(response, "request-body");
assert!(current().is_none());
}
#[tokio::test]
async fn scope_emits_on_cancellation() {
let (slot, emit) = capture();
let scope_fut = scope(emit, async {
wl_set!("status", "before-cancel");
tokio::task::yield_now().await;
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
wl_set!("status", "after-sleep");
});
tokio::select! {
_ = scope_fut => {
panic!("scope should not have completed");
}
_ = tokio::time::sleep(std::time::Duration::from_millis(50)) => {
}
}
let json = slot.lock().unwrap().clone().expect("emit was not called");
let parsed: sonic_rs::Value = sonic_rs::from_str(&json).unwrap();
assert_eq!(parsed["status"], "before-cancel");
assert!(parsed["duration"]["total_ms"].is_number());
assert!(parsed["event"]["timestamp"].is_str());
assert!(parsed["event"]["id"].is_str());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn one_thousand_concurrent_scopes() {
const N_TASKS: usize = 1000;
let slots: Vec<Arc<Mutex<Option<String>>>> =
(0..N_TASKS).map(|_| Arc::new(Mutex::new(None))).collect();
let mut handles = Vec::with_capacity(N_TASKS);
for (i, slot) in slots.iter().enumerate() {
let s = slot.clone();
handles.push(tokio::spawn(scope(
move |ev| *s.lock().unwrap() = Some(ev.to_json().unwrap()),
async move {
wl_set!("service.name", format!("task-{i}"));
wl_inc!("requests");
for _ in 0..3 {
tokio::task::yield_now().await;
}
wl_inc!("requests");
},
)));
}
for h in handles {
h.await.expect("task should not panic");
}
for (i, slot) in slots.iter().enumerate() {
let json = slot
.lock()
.unwrap()
.clone()
.expect("every task must emit exactly once");
let parsed: sonic_rs::Value =
sonic_rs::from_str(&json).expect("every emitted JSON must parse");
assert_eq!(
parsed["service"]["name"],
format!("task-{i}"),
"task {i} saw the wrong service.name (cross-task bleed)"
);
assert_eq!(
parsed["requests"], 2,
"task {i} should have incremented requests twice"
);
assert!(
parsed["event"]["id"].is_str(),
"task {i} should have an auto-generated event.id"
);
assert!(
parsed["event"]["timestamp"].is_str(),
"task {i} should have an auto-generated event.timestamp"
);
assert!(
parsed["duration"]["total_ms"].is_number(),
"task {i} should have a measured duration.total_ms"
);
}
}