#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
#![warn(
rustdoc::missing_crate_level_docs,
missing_docs,
nonstandard_style,
unused_qualifications
)]
#[cfg(test)]
#[doc = include_str!("../README.md")]
mod readme {}
pub use crate::formatters::{apache_combined, apache_common, dev_formatter};
use std::{
convert::AsMut,
fmt::{Display, Write},
io::IsTerminal,
sync::Arc,
};
use trillium::{Conn, Handler, Info, ListenerKind, Transport};
pub use trillium_logger_macros::log_format;
pub mod formatters;
#[cfg(feature = "client")]
#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
pub mod client;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
#[derive(Default)]
pub enum ColorMode {
#[default]
Auto,
On,
Off,
}
impl ColorMode {
pub(crate) fn is_enabled(&self) -> bool {
match self {
ColorMode::Auto => std::io::stdout().is_terminal(),
ColorMode::On => true,
ColorMode::Off => false,
}
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
#[derive(Default)]
pub enum Target {
Logger(log::Level),
#[default]
Stdout,
}
pub trait Targetable: Send + Sync + 'static {
fn write(&self, data: String);
}
impl Targetable for Target {
fn write(&self, data: String) {
match self {
Target::Logger(level) => {
log::log!(*level, "{}", data);
}
Target::Stdout => {
println!("{data}");
}
}
}
}
impl<F> Targetable for F
where
F: Fn(String) + Send + Sync + 'static,
{
fn write(&self, data: String) {
self(data);
}
}
pub trait LogFormatter: Send + Sync + 'static {
type Output: Display + Send + Sync + 'static;
fn format(&self, conn: &Conn, color: bool) -> Self::Output;
}
pub struct Logger<F> {
format: F,
color_mode: ColorMode,
target: Arc<dyn Targetable>,
init_message: bool,
}
impl Logger<()> {
pub fn new() -> Logger<impl LogFormatter> {
Logger {
format: dev_formatter,
color_mode: ColorMode::Auto,
target: Arc::new(Target::Stdout),
init_message: true,
}
}
}
impl<T> Logger<T> {
pub fn with_formatter<Formatter: LogFormatter>(
self,
formatter: Formatter,
) -> Logger<Formatter> {
Logger {
format: formatter,
color_mode: self.color_mode,
target: self.target,
init_message: self.init_message,
}
}
}
impl<F: LogFormatter> Logger<F> {
pub fn with_color_mode(mut self, color_mode: ColorMode) -> Self {
self.color_mode = color_mode;
self
}
pub fn with_target(mut self, target: impl Targetable) -> Self {
self.target = Arc::new(target);
self
}
pub fn without_init_message(mut self) -> Self {
self.init_message = false;
self
}
}
#[derive(Clone)]
pub struct LogTarget(Arc<dyn Targetable>);
impl Targetable for LogTarget {
fn write(&self, data: String) {
self.0.write(data);
}
}
impl LogTarget {
pub fn write(&self, data: String) {
self.0.write(data);
}
}
struct LoggerWasRun;
impl<F> Handler for Logger<F>
where
F: LogFormatter,
{
async fn init(&mut self, info: &mut Info) {
if self.init_message {
let mut string = "\nTrillium started\n".to_string();
if let Some(url) = info.shared_state::<url::Url>() {
writeln!(string, "✾ Listening at {}", url.as_str()).unwrap();
}
let mut bound: Vec<(String, bool)> = Vec::new();
for listener in info.listeners() {
let rendered = listener.to_string();
let is_h3 = matches!(listener.kind(), ListenerKind::Quic(_));
if let Some((_, h3)) = bound.iter_mut().find(|(r, _)| *r == rendered) {
*h3 |= is_h3;
} else {
bound.push((rendered, is_h3));
}
}
for (rendered, is_h3) in &bound {
let h3 = if *is_h3 { " (h3)" } else { "" };
writeln!(string, "✾ Bound to {rendered}{h3}").unwrap();
}
writeln!(string, "Control-c to quit").unwrap();
self.target.write(string);
}
info.insert_shared_state(LogTarget(Arc::clone(&self.target)));
}
async fn run(&self, conn: Conn) -> Conn {
conn.with_state(LoggerWasRun)
}
async fn before_send(&self, mut conn: Conn) -> Conn {
if conn.state::<LoggerWasRun>().is_some() {
let target = self.target.clone();
let output = self.format.format(&conn, self.color_mode.is_enabled());
let inner: &mut trillium_http::Conn<Box<dyn Transport>> = conn.as_mut();
inner.after_send(move |_| target.write(output.to_string()));
}
conn
}
}
pub fn logger() -> Logger<impl LogFormatter> {
Logger::new()
}