use std::collections::HashSet;
use std::env;
use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData;
use std::rc::Rc;
use actix_service::{Service, Transform};
use bytes::Bytes;
use futures::future::{ok, FutureResult};
use futures::{Async, Future, Poll};
use regex::Regex;
use time;
use crate::dev::{BodySize, MessageBody, ResponseBody};
use crate::error::{Error, Result};
use crate::service::{ServiceRequest, ServiceResponse};
use crate::{HttpMessage, HttpResponse};
pub struct Logger(Rc<Inner>);
struct Inner {
format: Format,
exclude: HashSet<String>,
}
impl Logger {
pub fn new(format: &str) -> Logger {
Logger(Rc::new(Inner {
format: Format::new(format),
exclude: HashSet::new(),
}))
}
pub fn exclude<T: Into<String>>(mut self, path: T) -> Self {
Rc::get_mut(&mut self.0)
.unwrap()
.exclude
.insert(path.into());
self
}
}
impl Default for Logger {
fn default() -> Logger {
Logger(Rc::new(Inner {
format: Format::default(),
exclude: HashSet::new(),
}))
}
}
impl<S, P, B> Transform<S> for Logger
where
S: Service<Request = ServiceRequest<P>, Response = ServiceResponse<B>>,
B: MessageBody,
{
type Request = ServiceRequest<P>;
type Response = ServiceResponse<StreamLog<B>>;
type Error = S::Error;
type InitError = ();
type Transform = LoggerMiddleware<S>;
type Future = FutureResult<Self::Transform, Self::InitError>;
fn new_transform(&self, service: S) -> Self::Future {
ok(LoggerMiddleware {
service,
inner: self.0.clone(),
})
}
}
pub struct LoggerMiddleware<S> {
inner: Rc<Inner>,
service: S,
}
impl<S, P, B> Service for LoggerMiddleware<S>
where
S: Service<Request = ServiceRequest<P>, Response = ServiceResponse<B>>,
B: MessageBody,
{
type Request = ServiceRequest<P>;
type Response = ServiceResponse<StreamLog<B>>;
type Error = S::Error;
type Future = LoggerResponse<S, P, B>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
self.service.poll_ready()
}
fn call(&mut self, req: ServiceRequest<P>) -> Self::Future {
if self.inner.exclude.contains(req.path()) {
LoggerResponse {
fut: self.service.call(req),
format: None,
time: time::now(),
_t: PhantomData,
}
} else {
let now = time::now();
let mut format = self.inner.format.clone();
for unit in &mut format.0 {
unit.render_request(now, &req);
}
LoggerResponse {
fut: self.service.call(req),
format: Some(format),
time: now,
_t: PhantomData,
}
}
}
}
#[doc(hidden)]
pub struct LoggerResponse<S, P, B>
where
B: MessageBody,
S: Service,
{
fut: S::Future,
time: time::Tm,
format: Option<Format>,
_t: PhantomData<(P, B)>,
}
impl<S, P, B> Future for LoggerResponse<S, P, B>
where
B: MessageBody,
S: Service<Request = ServiceRequest<P>, Response = ServiceResponse<B>>,
{
type Item = ServiceResponse<StreamLog<B>>;
type Error = S::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let res = futures::try_ready!(self.fut.poll());
if let Some(ref mut format) = self.format {
for unit in &mut format.0 {
unit.render_response(&res);
}
}
Ok(Async::Ready(res.map_body(move |_, body| {
ResponseBody::Body(StreamLog {
body,
size: 0,
time: self.time,
format: self.format.take(),
})
})))
}
}
pub struct StreamLog<B> {
body: ResponseBody<B>,
format: Option<Format>,
size: usize,
time: time::Tm,
}
impl<B> Drop for StreamLog<B> {
fn drop(&mut self) {
if let Some(ref format) = self.format {
let render = |fmt: &mut Formatter| {
for unit in &format.0 {
unit.render(fmt, self.size, self.time)?;
}
Ok(())
};
log::info!("{}", FormatDisplay(&render));
}
}
}
impl<B: MessageBody> MessageBody for StreamLog<B> {
fn length(&self) -> BodySize {
self.body.length()
}
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
match self.body.poll_next()? {
Async::Ready(Some(chunk)) => {
self.size += chunk.len();
Ok(Async::Ready(Some(chunk)))
}
val => Ok(val),
}
}
}
#[derive(Clone)]
#[doc(hidden)]
struct Format(Vec<FormatText>);
impl Default for Format {
fn default() -> Format {
Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
}
}
impl Format {
pub fn new(s: &str) -> Format {
log::trace!("Access log format: {}", s);
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap();
let mut idx = 0;
let mut results = Vec::new();
for cap in fmt.captures_iter(s) {
let m = cap.get(0).unwrap();
let pos = m.start();
if idx != pos {
results.push(FormatText::Str(s[idx..pos].to_owned()));
}
idx = m.end();
if let Some(key) = cap.get(2) {
results.push(match cap.get(3).unwrap().as_str() {
"i" => FormatText::RequestHeader(key.as_str().to_owned()),
"o" => FormatText::ResponseHeader(key.as_str().to_owned()),
"e" => FormatText::EnvironHeader(key.as_str().to_owned()),
_ => unreachable!(),
})
} else {
let m = cap.get(1).unwrap();
results.push(match m.as_str() {
"%" => FormatText::Percent,
"a" => FormatText::RemoteAddr,
"t" => FormatText::RequestTime,
"r" => FormatText::RequestLine,
"s" => FormatText::ResponseStatus,
"b" => FormatText::ResponseSize,
"T" => FormatText::Time,
"D" => FormatText::TimeMillis,
_ => FormatText::Str(m.as_str().to_owned()),
});
}
}
if idx != s.len() {
results.push(FormatText::Str(s[idx..].to_owned()));
}
Format(results)
}
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub enum FormatText {
Str(String),
Percent,
RequestLine,
RequestTime,
ResponseStatus,
ResponseSize,
Time,
TimeMillis,
RemoteAddr,
RequestHeader(String),
ResponseHeader(String),
EnvironHeader(String),
}
impl FormatText {
fn render(
&self,
fmt: &mut Formatter,
size: usize,
entry_time: time::Tm,
) -> Result<(), fmt::Error> {
match *self {
FormatText::Str(ref string) => fmt.write_str(string),
FormatText::Percent => "%".fmt(fmt),
FormatText::ResponseSize => size.fmt(fmt),
FormatText::Time => {
let rt = time::now() - entry_time;
let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0;
fmt.write_fmt(format_args!("{:.6}", rt))
}
FormatText::TimeMillis => {
let rt = time::now() - entry_time;
let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0;
fmt.write_fmt(format_args!("{:.6}", rt))
}
FormatText::EnvironHeader(ref name) => {
if let Ok(val) = env::var(name) {
fmt.write_fmt(format_args!("{}", val))
} else {
"-".fmt(fmt)
}
}
_ => Ok(()),
}
}
fn render_response<B>(&mut self, res: &HttpResponse<B>) {
match *self {
FormatText::ResponseStatus => {
*self = FormatText::Str(format!("{}", res.status().as_u16()))
}
FormatText::ResponseHeader(ref name) => {
let s = if let Some(val) = res.headers().get(name) {
if let Ok(s) = val.to_str() {
s
} else {
"-"
}
} else {
"-"
};
*self = FormatText::Str(s.to_string())
}
_ => (),
}
}
fn render_request<P>(&mut self, now: time::Tm, req: &ServiceRequest<P>) {
match *self {
FormatText::RequestLine => {
*self = if req.query_string().is_empty() {
FormatText::Str(format!(
"{} {} {:?}",
req.method(),
req.path(),
req.version()
))
} else {
FormatText::Str(format!(
"{} {}?{} {:?}",
req.method(),
req.path(),
req.query_string(),
req.version()
))
};
}
FormatText::RequestTime => {
*self = FormatText::Str(format!(
"{:?}",
now.strftime("[%d/%b/%Y:%H:%M:%S %z]").unwrap()
))
}
FormatText::RequestHeader(ref name) => {
let s = if let Some(val) = req.headers().get(name) {
if let Ok(s) = val.to_str() {
s
} else {
"-"
}
} else {
"-"
};
*self = FormatText::Str(s.to_string());
}
_ => (),
}
}
}
pub(crate) struct FormatDisplay<'a>(&'a Fn(&mut Formatter) -> Result<(), fmt::Error>);
impl<'a> fmt::Display for FormatDisplay<'a> {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
(self.0)(fmt)
}
}
#[cfg(test)]
mod tests {
use actix_service::{FnService, Service, Transform};
use super::*;
use crate::http::{header, StatusCode};
use crate::test::{block_on, TestRequest};
#[test]
fn test_logger() {
let srv = FnService::new(|req: ServiceRequest<_>| {
req.into_response(
HttpResponse::build(StatusCode::OK)
.header("X-Test", "ttt")
.finish(),
)
});
let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test");
let mut srv = block_on(logger.new_transform(srv)).unwrap();
let req = TestRequest::with_header(
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
)
.to_service();
let _res = block_on(srv.call(req));
}
}