1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
/*!
Trillium crate to add identifiers to conns.
This crate provides the following utilities:
* [`ConnId`] a handler which must be called for the rest of this crate to function
* [`log_formatter::conn_id`] a formatter to use with trillium_logger
(note that this does not depend on the trillium_logger crate and is very lightweight
if you do not use that crate)
* [`ConnIdExt`] an extension trait for retrieving the id from a conn
*/
#![forbid(unsafe_code)]
#![deny(
missing_copy_implementations,
rustdoc::missing_crate_level_docs,
missing_debug_implementations,
missing_docs,
nonstandard_style,
unused_qualifications
)]
use fastrand::Rng;
use std::{
fmt::{Debug, Formatter, Result},
iter::repeat_with,
ops::Deref,
sync::{Arc, Mutex},
};
use trillium::{async_trait, Conn, Handler, HeaderName, KnownHeaderName, StateSet};
#[derive(Default)]
enum IdGenerator {
#[default]
Default,
SeededFastrand(Arc<Mutex<Rng>>),
Fn(Box<dyn Fn() -> String + Send + Sync + 'static>),
}
impl IdGenerator {
fn generate(&self) -> Id {
match self {
IdGenerator::Default => Id::default(),
IdGenerator::SeededFastrand(rng) => Id::with_rng(&mut rng.lock().unwrap()),
IdGenerator::Fn(gen_fun) => Id(gen_fun()),
}
}
}
/**
Trillium handler to set a identifier for every Conn.
By default, it will use an inbound `x-request-id` request header or if
that is missing, populate a ten character random id. This handler will
set an outbound `x-request-id` header as well by default. All of this
behavior can be customized with [`ConnId::with_request_header`],
[`ConnId::with_response_header`] and [`ConnId::with_id_generator`]
*/
pub struct ConnId {
request_header: Option<HeaderName<'static>>,
response_header: Option<HeaderName<'static>>,
id_generator: IdGenerator,
}
impl Debug for ConnId {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("ConnId")
.field("request_header", &self.request_header)
.field("response_header", &self.response_header)
.field("id_generator", &self.id_generator)
.finish()
}
}
impl Default for ConnId {
fn default() -> Self {
Self {
request_header: Some(KnownHeaderName::XrequestId.into()),
response_header: Some(KnownHeaderName::XrequestId.into()),
id_generator: Default::default(),
}
}
}
impl ConnId {
/**
Constructs a new ConnId handler
```
# use trillium_testing::prelude::*;
# use trillium_conn_id::ConnId;
let app = (ConnId::new().with_seed(1000), "ok"); // seeded for testing
assert_ok!(
get("/").on(&app),
"ok",
"x-request-id" => "J4lzoPXcT5"
);
assert_headers!(
get("/")
.with_request_header("x-request-id", "inbound")
.on(&app),
"x-request-id" => "inbound"
);
```
*/
pub fn new() -> Self {
Self::default()
}
/**
Specifies a request header to use. If this header is provided on
the inbound request, the id will be used unmodified. To disable
this behavior, see [`ConnId::without_request_header`]
```
# use trillium_testing::prelude::*;
# use trillium_conn_id::ConnId;
let app = (
ConnId::new().with_request_header("x-custom-id"),
"ok"
);
assert_headers!(
get("/")
.with_request_header("x-custom-id", "inbound")
.on(&app),
"x-request-id" => "inbound"
);
```
*/
pub fn with_request_header(mut self, request_header: impl Into<HeaderName<'static>>) -> Self {
self.request_header = Some(request_header.into());
self
}
/**
disables the default behavior of reusing an inbound header for
the request id. If a ConnId is configured
`without_request_header`, a new id will always be generated
*/
pub fn without_request_header(mut self) -> Self {
self.request_header = None;
self
}
/**
Specifies a response header to set. To disable this behavior, see
[`ConnId::without_response_header`]
```
# use trillium_testing::prelude::*;
# use trillium_conn_id::ConnId;
let app = (
ConnId::new()
.with_seed(1000) // for testing
.with_response_header("x-custom-header"),
"ok"
);
assert_headers!(
get("/").on(&app),
"x-custom-header" => "J4lzoPXcT5"
);
```
*/
pub fn with_response_header(mut self, response_header: impl Into<HeaderName<'static>>) -> Self {
self.response_header = Some(response_header.into());
self
}
/**
Disables the default behavior of sending the conn id as a response
header. A request id will be available within the application
through use of [`ConnIdExt`] but will not be sent as part of the
response.
*/
pub fn without_response_header(mut self) -> Self {
self.response_header = None;
self
}
/**
Provide an alternative generator function for ids. The default
is a ten-character alphanumeric random sequence.
```
# use trillium_testing::prelude::*;
# use trillium_conn_id::ConnId;
# use uuid::Uuid;
let app = (
ConnId::new().with_id_generator(|| Uuid::new_v4().to_string()),
"ok"
);
// assert that the id is a valid uuid, even if we can't assert a specific value
assert!(Uuid::parse_str(get("/").on(&app).response_headers().get_str("x-request-id").unwrap()).is_ok());
```
*/
pub fn with_id_generator<F>(mut self, id_generator: F) -> Self
where
F: Fn() -> String + Send + Sync + 'static,
{
self.id_generator = IdGenerator::Fn(Box::new(id_generator));
self
}
/// seed a shared rng
///
/// this is primarily useful for tests
pub fn with_seed(mut self, seed: u64) -> Self {
self.id_generator = IdGenerator::SeededFastrand(Arc::new(Mutex::new(Rng::with_seed(seed))));
self
}
fn generate_id(&self) -> Id {
self.id_generator.generate()
}
}
#[derive(Clone, Debug)]
struct Id(String);
impl Deref for Id {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Display for Id {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_str(self)
}
}
impl Default for Id {
fn default() -> Self {
Self(repeat_with(fastrand::alphanumeric).take(10).collect())
}
}
impl Id {
fn with_rng(rng: &mut Rng) -> Self {
Self(repeat_with(|| rng.alphanumeric()).take(10).collect())
}
}
#[async_trait]
impl Handler for ConnId {
async fn run(&self, mut conn: Conn) -> Conn {
let id = self
.request_header
.as_ref()
.and_then(|request_header| conn.request_headers().get_str(request_header.clone()))
.map(|request_header| Id(request_header.to_string()))
.unwrap_or_else(|| self.generate_id());
if let Some(ref response_header) = self.response_header {
conn.response_headers_mut()
.insert(response_header.clone(), id.to_string());
}
conn.with_state(id)
}
}
/// Extension trait to retrieve an id generated by the [`ConnId`] handler
pub trait ConnIdExt {
/// Retrieves the id for this conn. This method will panic if it
/// is run before the [`ConnId`] handler.
fn id(&self) -> &str;
}
impl<ConnLike> ConnIdExt for ConnLike
where
ConnLike: AsRef<StateSet>,
{
fn id(&self) -> &str {
self.as_ref()
.get::<Id>()
.expect("ConnId handler must be run before calling IdConnExt::id")
}
}
/// Formatter for the trillium_log crate
pub mod log_formatter {
use std::borrow::Cow;
use super::*;
/// Formatter for the trillium_log crate. This will be `-` if
/// there is no id on the conn.
pub fn conn_id(conn: &Conn, _color: bool) -> Cow<'static, str> {
conn.state::<Id>()
.map(|id| Cow::Owned(id.0.clone()))
.unwrap_or_else(|| Cow::Borrowed("-"))
}
}
/// Alias for ConnId::new()
pub fn conn_id() -> ConnId {
ConnId::new()
}
impl Debug for IdGenerator {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_str(match self {
IdGenerator::Default => "IdGenerator::Default",
IdGenerator::SeededFastrand(_) => "IdGenerator::SeededFastrand",
IdGenerator::Fn(_) => "IdGenerator::Fn",
})
}
}