Instrument with tracing

This commit is contained in:
Aode (lion) 2021-09-19 13:21:23 -05:00
parent 710fcd0f3d
commit f0c6bc24f5
3 changed files with 831 additions and 112 deletions

771
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,15 +18,30 @@ actix-web = { version = "4.0.0-beta.8", default-features = false }
anyhow = "1.0"
awc = { version = "3.0.0-beta.7", default-features = false }
dotenv = "0.15.0"
env_logger = "0.9"
mime = "0.3"
minify-html = "0.6.8"
once_cell = "1.4"
opentelemetry = { version = "0.16", features = ["rt-tokio"] }
opentelemetry-otlp = "0.9"
serde = { version = "1.0", features = ["derive"] }
structopt = "0.3.14"
thiserror = "1.0"
tracing = "0.1"
tracing-awc = { version = "0.1.0-beta.4", features = ["opentelemetry_0_16"] }
tracing-error = "0.1"
tracing-futures = "0.2"
tracing-log = "0.1"
tracing-opentelemetry = "0.15"
tracing-subscriber = { version = "0.2", features = ["ansi", "fmt"] }
url = "2.1"
[dependencies.tracing-actix-web]
version = "0.4.0-beta.12"
git = "https://github.com/asonix/tracing-actix-web"
branch = "asonix/tracing-error-work-around"
features = ["emit_event_on_error", "opentelemetry_0_16"]
[build-dependencies]
anyhow = "1.0"
dotenv = "0.15.0"

View file

@ -4,17 +4,23 @@ use actix_web::{
header::{CacheControl, CacheDirective, ContentType, LastModified, LOCATION},
StatusCode,
},
middleware::Logger,
web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer, ResponseError,
};
use awc::Client;
use once_cell::sync::Lazy;
use opentelemetry::{sdk::{Resource, propagation::TraceContextPropagator}, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use std::{
io::Cursor,
net::SocketAddr,
time::{Duration, SystemTime},
};
use structopt::StructOpt;
use tracing_actix_web::TracingLogger;
use tracing_awc::Propagate;
use tracing_error::{ErrorLayer, SpanTrace};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt::format::FmtSpan, layer::SubscriberExt, EnvFilter, Registry};
use url::Url;
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
@ -50,6 +56,14 @@ struct Config {
help = "The scheme, domain, and optional port of the pict-rs proxy server"
)]
domain: Url,
#[structopt(
short,
long,
env = "PICTRS_PROXY_OPENTELEMETRY_URL",
help = "URL of OpenTelemetry Collector"
)]
opentelemetry_url: Option<Url>,
}
impl Config {
@ -141,7 +155,7 @@ impl Config {
static CONFIG: Lazy<Config> = Lazy::new(Config::from_args);
#[derive(serde::Deserialize)]
#[derive(Debug, serde::Deserialize)]
enum FileType {
#[serde(rename = "jpg")]
Jpg,
@ -240,8 +254,56 @@ fn statics(file: &str) -> String {
format!("/static/{}", file)
}
#[derive(Debug)]
struct Error {
context: SpanTrace,
kind: ErrorKind,
}
impl<T> From<T> for Error
where
ErrorKind: From<T>,
{
fn from(error: T) -> Self {
Error {
context: SpanTrace::capture(),
kind: error.into(),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.kind)?;
std::fmt::Display::fmt(&self.context, f)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.kind.source()
}
}
impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
fn error_response(&self) -> HttpResponse {
match render(HttpResponse::build(self.status_code()), |cursor| {
self::templates::error(cursor, &self.kind.to_string())
}) {
Ok(res) => res,
Err(_) => HttpResponse::build(self.status_code())
.content_type(mime::TEXT_PLAIN.essence_str())
.body(self.kind.to_string()),
}
}
}
#[derive(Debug, thiserror::Error)]
enum Error {
enum ErrorKind {
#[error("{0}")]
Io(#[from] std::io::Error),
@ -252,29 +314,14 @@ enum Error {
JsonPayload(#[from] awc::error::JsonPayloadError),
}
impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
fn error_response(&self) -> HttpResponse {
match render(HttpResponse::build(self.status_code()), |cursor| {
self::templates::error(cursor, &self.to_string())
}) {
Ok(res) => res,
Err(_) => HttpResponse::build(self.status_code())
.content_type(mime::TEXT_PLAIN.essence_str())
.body(self.to_string()),
}
}
}
#[tracing::instrument(name = "Upload")]
async fn index() -> Result<HttpResponse, Error> {
render(HttpResponse::Ok(), |cursor| {
self::templates::index(cursor, "/upload", "images[]")
})
}
#[tracing::instrument(name = "Upload", skip(req, body, client))]
async fn upload(
req: HttpRequest,
body: web::Payload,
@ -283,12 +330,12 @@ async fn upload(
let client_request = client.request_from(CONFIG.upstream_upload_url(), req.head());
let client_request = if let Some(addr) = req.head().peer_addr {
client_request.insert_header(("X-Forwarded-For", addr.to_string()))
client_request.append_header(("X-Forwarded-For", addr.to_string()))
} else {
client_request
};
let mut res = client_request.send_stream(body).await?;
let mut res = client_request.propagate().send_stream(body).await?;
let images = res.json::<Images>().await?;
@ -304,6 +351,7 @@ struct ThumbnailQuery {
image: String,
}
#[tracing::instrument(name = "Thumbs", skip(client))]
async fn thumbs(
query: web::Query<ThumbnailQuery>,
client: web::Data<Client>,
@ -311,7 +359,7 @@ async fn thumbs(
let file = query.into_inner().image;
let url = CONFIG.upstream_details_url(&file);
let mut res = client.get(url).send().await?;
let mut res = client.get(url).propagate().send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
@ -330,6 +378,7 @@ async fn thumbs(
})
}
#[tracing::instrument(name = "Image", skip(req, client))]
async fn image(
url: String,
req: HttpRequest,
@ -342,7 +391,7 @@ async fn image(
client_request
};
let res = client_request.no_decompress().send().await?;
let res = client_request.no_decompress().propagate().send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
@ -357,6 +406,7 @@ async fn image(
Ok(client_res.body(BodyStream::new(res)))
}
#[tracing::instrument(name = "View original", skip(client))]
async fn view_original(
file: web::Path<String>,
client: web::Data<Client>,
@ -364,7 +414,7 @@ async fn view_original(
let file = file.into_inner();
let url = CONFIG.upstream_details_url(&file);
let mut res = client.get(url).send().await?;
let mut res = client.get(url).propagate().send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
@ -383,6 +433,7 @@ async fn view_original(
})
}
#[tracing::instrument(name = "View", skip(client))]
async fn view(
parts: web::Path<(u64, String)>,
client: web::Data<Client>,
@ -394,7 +445,7 @@ async fn view(
}
let url = CONFIG.upstream_details_url(&file);
let mut res = client.get(url).send().await?;
let mut res = client.get(url).propagate().send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
@ -413,6 +464,7 @@ async fn view(
})
}
#[tracing::instrument(name = "Thumbnail", skip(req, client))]
async fn thumbnail(
parts: web::Path<(u64, FileType, String)>,
req: HttpRequest,
@ -433,6 +485,7 @@ fn valid_thumbnail_size(size: u64) -> bool {
THUMBNAIL_SIZES.contains(&size)
}
#[tracing::instrument(name = "Full resolution", skip(req, client))]
async fn full_res(
filename: web::Path<String>,
req: HttpRequest,
@ -443,6 +496,7 @@ async fn full_res(
image(url, req, client).await
}
#[tracing::instrument(name = "Static files")]
async fn static_files(filename: web::Path<String>) -> HttpResponse {
let filename = filename.into_inner();
@ -470,6 +524,7 @@ struct DeleteQuery {
confirm: bool,
}
#[tracing::instrument(name = "Delete", skip(client))]
async fn delete(
query: web::Query<DeleteQuery>,
client: web::Data<Client>,
@ -481,7 +536,7 @@ async fn delete(
} = query.into_inner();
let url = CONFIG.upstream_details_url(&file);
let mut res = client.get(url).send().await?;
let mut res = client.get(url).propagate().send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(to_404());
@ -489,7 +544,7 @@ async fn delete(
if confirm {
let url = CONFIG.upstream_delete_url(&token, &file);
client.delete(url).send().await?;
client.delete(url).propagate().send().await?;
render(HttpResponse::Ok(), |cursor| {
self::templates::deleted(cursor, &file)
@ -516,6 +571,7 @@ fn to_404() -> HttpResponse {
.finish()
}
#[tracing::instrument(name = "Not Found")]
async fn not_found() -> Result<HttpResponse, Error> {
render(HttpResponse::NotFound(), |cursor| {
self::templates::not_found(cursor)
@ -528,6 +584,7 @@ async fn go_home() -> HttpResponse {
.finish()
}
#[tracing::instrument(name = "Render", skip(builder, f))]
fn render(
mut builder: HttpResponseBuilder,
f: impl FnOnce(&mut Cursor<&mut Vec<u8>>) -> Result<(), std::io::Error>,
@ -547,11 +604,43 @@ fn render(
async fn main() -> Result<(), anyhow::Error> {
dotenv::dotenv().ok();
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
env_logger::init();
LogTracer::init()?;
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let format_layer = tracing_subscriber::fmt::layer()
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.pretty();
let subscriber = Registry::default()
.with(env_filter)
.with(format_layer)
.with(ErrorLayer::default());
if let Some(url) = &CONFIG.opentelemetry_url {
let tracer =
opentelemetry_otlp::new_pipeline()
.tracing()
.with_trace_config(opentelemetry::sdk::trace::config().with_resource(
Resource::new(vec![KeyValue::new("service.name", "pict-rs-proxy")]),
))
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint(url.as_str()),
)
.install_batch(opentelemetry::runtime::Tokio)?;
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber = subscriber.with(otel_layer);
tracing::subscriber::set_global_default(subscriber)?;
} else {
tracing::subscriber::set_global_default(subscriber)?;
}
HttpServer::new(move || {
let client = Client::builder()
@ -561,7 +650,7 @@ async fn main() -> Result<(), anyhow::Error> {
App::new()
.app_data(web::Data::new(client))
.wrap(Logger::default())
.wrap(TracingLogger::default())
.service(web::resource("/").route(web::get().to(index)))
.service(web::resource("/upload").route(web::post().to(upload)))
.service(web::resource("/image/{filename}").route(web::get().to(full_res)))