Skip to content

Commit 25de413

Browse files
committed
Rewrite tide-slog to setup a per-request logger instance and support slog-scope
1 parent d17b4f0 commit 25de413

File tree

6 files changed

+259
-64
lines changed

6 files changed

+259
-64
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,8 @@ matrix:
5454
- name: cargo test
5555
script:
5656
- cargo test -Zmtime-on-use --all --verbose
57+
58+
- name: cargo test --all-features
59+
script:
60+
- cargo test -Zmtime-on-use --all-features
61+
- cargo test -Zmtime-on-use --manifest-path tide-slog/Cargo.toml --all-features

tide-slog/Cargo.toml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
[package]
2-
authors = [
3-
"Tide Developers"
4-
]
2+
authors = ["Tide Developers"]
53
description = "Logging middleware for Tide based on slog"
64
documentation = "https://docs.rs/tide-slog"
75
keywords = ["tide", "web", "middleware", "logging", "slog"]
@@ -17,14 +15,23 @@ readme = "README.md"
1715
repository = "https://github.com/rustasync/tide"
1816
version = "0.1.0"
1917

18+
[package.metadata.docs.rs]
19+
all-features = true
20+
rustdoc-args = ["--cfg docrs"]
21+
22+
[features]
23+
scope = ["slog-scope", "slog-scope-futures"]
24+
2025
[dependencies]
2126
tide-core = { path = "../tide-core", default-features = false }
2227
futures-preview = "0.3.0-alpha.17"
2328
http = "0.1"
24-
log = "0.4.6"
2529
slog = "2.4.1"
26-
slog-async = "2.3.0"
27-
slog-term = "2.4.0"
30+
slog-scope = { version = "4.1.1", optional = true }
31+
slog-scope-futures = { version = "0.1.1", optional = true }
2832

2933
[dev-dependencies]
3034
tide = { path = "../", default-features = false }
35+
uuid = { version = "0.7.4", default-features = false, features = ["v4"] }
36+
slog-stdlog = "3.0.2"
37+
slog-scope = "4.1.1"

tide-slog/src/lib.rs

Lines changed: 27 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Crate that provides helpers and/or middlewares for Tide
22
//! related to structured logging with slog.
33
4+
#![cfg_attr(docrs, feature(doc_cfg))]
45
#![feature(async_await)]
56
#![warn(
67
nonstandard_style,
@@ -9,65 +10,33 @@
910
missing_debug_implementations
1011
)]
1112

12-
use slog::{info, o, trace, Drain};
13-
use slog_async;
14-
use slog_term;
15-
16-
use futures::future::BoxFuture;
17-
18-
use tide_core::{
19-
middleware::{Middleware, Next},
20-
Context, Response,
21-
};
22-
23-
/// RequestLogger based on slog.SimpleLogger
24-
#[derive(Debug)]
25-
pub struct RequestLogger {
26-
// drain: dyn slog::Drain,
27-
inner: slog::Logger,
28-
}
29-
30-
impl RequestLogger {
31-
pub fn new() -> Self {
32-
Default::default()
33-
}
34-
35-
pub fn with_logger(logger: slog::Logger) -> Self {
36-
Self { inner: logger }
37-
}
38-
}
39-
40-
impl Default for RequestLogger {
41-
fn default() -> Self {
42-
let decorator = slog_term::TermDecorator::new().build();
43-
let drain = slog_term::CompactFormat::new(decorator).build().fuse();
44-
let drain = slog_async::Async::new(drain).build().fuse();
45-
46-
let log = slog::Logger::root(drain, o!());
47-
Self { inner: log }
48-
}
13+
mod per_request_logger;
14+
mod request_logger;
15+
#[cfg(feature = "scope")]
16+
mod set_slog_scope_logger;
17+
18+
pub use per_request_logger::PerRequestLogger;
19+
pub use request_logger::RequestLogger;
20+
#[cfg(feature = "scope")]
21+
pub use set_slog_scope_logger::SetSlogScopeLogger;
22+
23+
use tide_core::Context;
24+
25+
/// An extension to [`Context`] that provides access to a per-request [`slog::Logger`]
26+
pub trait ContextExt {
27+
/// Returns a [`slog::Logger`] scoped to this request.
28+
///
29+
/// # Panics
30+
///
31+
/// Will panic if no [`PerRequestLogger`] middleware has been used to setup the request scoped
32+
/// logger.
33+
fn logger(&self) -> &slog::Logger;
4934
}
5035

51-
/// Stores information during request phase and logs information once the response
52-
/// is generated.
53-
impl<State: Send + Sync + 'static> Middleware<State> for RequestLogger {
54-
fn handle<'a>(&'a self, cx: Context<State>, next: Next<'a, State>) -> BoxFuture<'a, Response> {
55-
Box::pin(async move {
56-
let path = cx.uri().path().to_owned();
57-
let method = cx.method().as_str().to_owned();
58-
trace!(self.inner, "IN => {} {}", method, path);
59-
let start = std::time::Instant::now();
60-
let res = next.run(cx).await;
61-
let status = res.status();
62-
info!(
63-
self.inner,
64-
"{} {} {} {}ms",
65-
method,
66-
path,
67-
status.as_str(),
68-
start.elapsed().as_millis()
69-
);
70-
res
71-
})
36+
impl<State> ContextExt for Context<State> {
37+
fn logger(&self) -> &slog::Logger {
38+
self.extensions()
39+
.get::<slog::Logger>()
40+
.expect("PerRequestLogger must be used to populate request logger")
7241
}
7342
}

tide-slog/src/per_request_logger.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use futures::future::BoxFuture;
2+
use tide_core::{
3+
middleware::{Middleware, Next},
4+
Context, Response,
5+
};
6+
7+
/// Middleware that injects a per-request [`slog::Logger`] onto the request [`Context`].
8+
pub struct PerRequestLogger<State> {
9+
setup: Box<dyn (Fn(&mut Context<State>) -> slog::Logger) + Send + Sync + 'static>,
10+
}
11+
12+
impl<State> PerRequestLogger<State> {
13+
/// Initialize this middleware with a function to create a per-request logger.
14+
///
15+
/// # Examples
16+
///
17+
/// ## Adding a base16 encoded per-request UUID onto the logger context
18+
///
19+
/// ```
20+
/// use slog::o;
21+
///
22+
/// let mut app = tide::App::new();
23+
///
24+
/// let request_id = || uuid::Uuid::new_v4().to_simple().to_string();
25+
///
26+
/// let root_logger = slog::Logger::root(slog::Discard, o!());
27+
/// app.middleware(tide_slog::PerRequestLogger::with_setup(move |_cx| root_logger.new(o! {
28+
/// "request" => request_id(),
29+
/// })));
30+
/// ```
31+
///
32+
/// ## Taking an externally provided request id from headers for the logger context
33+
///
34+
/// ```
35+
/// use slog::o;
36+
///
37+
/// let mut app = tide::App::new();
38+
///
39+
/// let root_logger = slog::Logger::root(slog::Discard, o!());
40+
/// app.middleware(tide_slog::PerRequestLogger::with_setup(move |cx| root_logger.new(o! {
41+
/// "request" => cx.headers().get("Request-Id").unwrap().to_str().unwrap().to_owned(),
42+
/// })));
43+
/// ```
44+
pub fn with_setup(
45+
setup: impl (Fn(&mut Context<State>) -> slog::Logger) + Send + Sync + 'static,
46+
) -> Self {
47+
Self {
48+
setup: Box::new(setup),
49+
}
50+
}
51+
52+
/// Initialize this middleware with a logger that will provided to each request.
53+
///
54+
/// # Examples
55+
///
56+
/// ```
57+
/// use slog::o;
58+
///
59+
/// let mut app = tide::App::new();
60+
///
61+
/// let root_logger = slog::Logger::root(slog::Discard, o!());
62+
/// app.middleware(tide_slog::PerRequestLogger::with_logger(root_logger));
63+
/// ```
64+
pub fn with_logger(logger: slog::Logger) -> Self {
65+
Self {
66+
setup: Box::new(move |_cx| logger.clone()),
67+
}
68+
}
69+
}
70+
71+
impl<State: Send + Sync + 'static> Middleware<State> for PerRequestLogger<State> {
72+
fn handle<'a>(
73+
&'a self,
74+
mut cx: Context<State>,
75+
next: Next<'a, State>,
76+
) -> BoxFuture<'a, Response> {
77+
let logger = (self.setup)(&mut cx);
78+
cx.extensions_mut().insert(logger);
79+
next.run(cx)
80+
}
81+
}
82+
83+
impl<State> std::fmt::Debug for PerRequestLogger<State> {
84+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85+
f.debug_struct("PerRequestLogger")
86+
.field("setup", &"[closure]")
87+
.finish()
88+
}
89+
}

tide-slog/src/request_logger.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use crate::ContextExt;
2+
use futures::future::BoxFuture;
3+
use slog::{info, trace};
4+
use tide_core::{
5+
middleware::{Middleware, Next},
6+
Context, Response,
7+
};
8+
9+
/// Middleware that logs minimal request details to the current request's [`slog::Logger`].
10+
///
11+
/// Relies on having a [`PerRequestLogger`][crate::PerRequestLogger] middleware instance setup
12+
/// beforehand to get the logger from.
13+
///
14+
/// # Examples
15+
///
16+
/// ```
17+
/// use slog::o;
18+
///
19+
/// let mut app = tide::App::new();
20+
///
21+
/// let root_logger = slog::Logger::root(slog::Discard, o!());
22+
/// app.middleware(tide_slog::PerRequestLogger::with_logger(root_logger));
23+
/// app.middleware(tide_slog::RequestLogger::new());
24+
/// ```
25+
#[derive(Debug)]
26+
pub struct RequestLogger {
27+
// In case we want to make this configurable in the future
28+
_reserved: (),
29+
}
30+
31+
impl RequestLogger {
32+
/// Create a new [`RequestLogger`] instance.
33+
pub fn new() -> Self {
34+
Self { _reserved: () }
35+
}
36+
}
37+
38+
impl<State: Send + Sync + 'static> Middleware<State> for RequestLogger {
39+
fn handle<'a>(&'a self, cx: Context<State>, next: Next<'a, State>) -> BoxFuture<'a, Response> {
40+
Box::pin(async move {
41+
let logger = cx.logger().clone();
42+
let path = cx.uri().path().to_owned();
43+
let method = cx.method().as_str().to_owned();
44+
45+
trace!(
46+
logger,
47+
"IN => {method} {path}",
48+
method = &method,
49+
path = &path
50+
);
51+
52+
let start = std::time::Instant::now();
53+
let res = next.run(cx).await;
54+
let status = res.status();
55+
56+
info!(
57+
logger,
58+
"{method} {path} {status} {elapsed}ms",
59+
method = &method,
60+
path = &path,
61+
status = status.as_str(),
62+
elapsed = start.elapsed().as_millis(),
63+
);
64+
65+
res
66+
})
67+
}
68+
}
69+
70+
impl Default for RequestLogger {
71+
fn default() -> Self {
72+
Self::new()
73+
}
74+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use crate::ContextExt;
2+
use futures::future::{BoxFuture, FutureExt as _};
3+
use slog_scope_futures::FutureExt as _;
4+
use tide_core::{
5+
middleware::{Middleware, Next},
6+
Response,
7+
};
8+
9+
#[cfg_attr(docrs, doc(cfg(feature = "scope")))]
10+
/// Middleware that ensures the current request's [`slog::Logger`] will be accessible using
11+
/// [`slog-scope::logger`] during all following processing of the request.
12+
///
13+
/// Relies on having a [`PerRequestLogger`][crate::PerRequestLogger] middleware instance setup
14+
/// beforehand to get the logger from.
15+
///
16+
/// This can be used along with [`slog-stdlog`](https://docs.rs/slog-stdlog/) to
17+
/// integrate per-request logging with middleware that use [`log`](https://docs.rs/log)`.
18+
///
19+
/// # Examples
20+
///
21+
/// ```
22+
/// use slog::o;
23+
///
24+
/// let root_logger = slog::Logger::root(slog::Discard, o!());
25+
///
26+
/// let _guard = slog_scope::set_global_logger(root_logger.clone());
27+
/// slog_stdlog::init()?;
28+
///
29+
/// let mut app = tide::App::new();
30+
///
31+
/// app.middleware(tide_slog::PerRequestLogger::with_logger(root_logger));
32+
/// app.middleware(tide_slog::SetSlogScopeLogger);
33+
///
34+
/// // The default tide request logger uses `log`, but since we are using `slog-stdlog` and run
35+
/// // `SetSlogScopeLogger` first it will be redirected into the per-request logger instance.
36+
/// app.middleware(tide::middleware::RequestLogger::new());
37+
/// # Ok::<(), Box<dyn std::error::Error>>(())
38+
/// ```
39+
#[derive(Debug)]
40+
pub struct SetSlogScopeLogger;
41+
42+
impl<State: Send + Sync + 'static> Middleware<State> for SetSlogScopeLogger {
43+
fn handle<'a>(
44+
&'a self,
45+
cx: tide_core::Context<State>,
46+
next: Next<'a, State>,
47+
) -> BoxFuture<'a, Response> {
48+
let logger = cx.logger().clone();
49+
next.run(cx).with_logger(logger).boxed()
50+
}
51+
}

0 commit comments

Comments
 (0)