Compare commits

..

No commits in common. "aa5aaf816ff6ed7a0b2e2ca56bb61ee0322e9af3" and "192f0e757aae18f20c9b5bde10e5a92982e6a42d" have entirely different histories.

5 changed files with 48 additions and 833 deletions

3
.envrc
View file

@ -1,3 +0,0 @@
watch_file flake.nix flake.lock
dotenv_if_exists
use flake

1
.gitignore vendored
View file

@ -1,2 +1 @@
target target
.env

756
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,8 +9,6 @@ artem = { version = "=1.1.5", default-features = false }
axum = "0.6" axum = "0.6"
color-eyre = "0.6" color-eyre = "0.6"
image = "0.24" image = "0.24"
opentelemetry = { version = "0.18", features = ["rt-tokio"] }
opentelemetry-honeycomb = { git = "https://github.com/fasterthanlime/opentelemetry-honeycomb-rs", branch = "simplified", version = "0.1.0" }
pretty-hex = "0.3" pretty-hex = "0.3"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }

View file

@ -2,35 +2,18 @@ use std::str::FromStr;
use axum::{ use axum::{
body::BoxBody, body::BoxBody,
extract::State, http::header,
http::header::{self, HeaderMap},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing::get, routing::get,
Router, Router,
}; };
use opentelemetry::{
global,
trace::{get_active_span, FutureExt, Span, Status, TraceContextExt, Tracer},
Context, KeyValue,
};
use reqwest::StatusCode; use reqwest::StatusCode;
use serde::Deserialize; use serde::Deserialize;
use tracing::{info, Level}; use tracing::{info, Level};
use tracing_subscriber::{filter::Targets, layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{filter::Targets, layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Clone)]
struct ServerState {
client: reqwest::Client,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let (_honeyguard, _tracer) = opentelemetry_honeycomb::new_pipeline(
std::env::var("HONEYCOMB_API_KEY").expect("$HONEYCOMB_API_KEY should be set"),
"catscii".into(),
)
.install()
.unwrap();
let filter = Targets::from_str(std::env::var("RUST_LOG").as_deref().unwrap_or("info")) let filter = Targets::from_str(std::env::var("RUST_LOG").as_deref().unwrap_or("info"))
.expect("RUST_LOG should be a valid tracing filter"); .expect("RUST_LOG should be a valid tracing filter");
tracing_subscriber::fmt() tracing_subscriber::fmt()
@ -40,51 +23,18 @@ async fn main() {
.with(filter) .with(filter)
.init(); .init();
let state = ServerState { let app = Router::new().route("/", get(root_get));
client: Default::default(),
};
let app = Router::new().route("/", get(root_get)).with_state(state);
let quit_signal = async {
_ = tokio::signal::ctrl_c().await;
eprintln!("Initializing graceful shutdown");
};
let addr = "0.0.0.0:8080".parse().unwrap(); let addr = "0.0.0.0:8080".parse().unwrap();
info!("Listening on {addr}"); info!("Listening on {addr}");
axum::Server::bind(&addr) axum::Server::bind(&addr)
.serve(app.into_make_service()) .serve(app.into_make_service())
.with_graceful_shutdown(quit_signal)
.await .await
.unwrap(); .unwrap();
} }
async fn root_get(headers: HeaderMap, State(state): State<ServerState>) -> Response<BoxBody> { async fn root_get() -> Response<BoxBody> {
let tracer = global::tracer(""); match get_cat_ascii_art().await {
let mut span = tracer.start("root_get");
span.set_attribute(KeyValue::new(
"user_agent",
headers
.get(header::USER_AGENT)
.map(|h| h.to_str().unwrap_or_default().to_owned())
.unwrap_or_default(),
));
root_get_inner(state)
.with_context(Context::current_with_span(span))
.await
}
async fn root_get_inner(state: ServerState) -> Response<BoxBody> {
let tracer = global::tracer("");
match get_cat_ascii_art(&state.client)
.with_context(Context::current_with_span(
tracer.start("get_cat_arscii_art"),
))
.await
{
Ok(art) => ( Ok(art) => (
StatusCode::OK, StatusCode::OK,
[(header::CONTENT_TYPE, "text/html; charset=utf-8")], [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
@ -92,57 +42,20 @@ async fn root_get_inner(state: ServerState) -> Response<BoxBody> {
) )
.into_response(), .into_response(),
Err(e) => { Err(e) => {
get_active_span(|span| { println!("Something went wrong: {e}");
span.set_status(Status::Error {
description: format!("{e}").into(),
})
});
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response() (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response()
} }
} }
} }
async fn get_cat_ascii_art(client: &reqwest::Client) -> color_eyre::Result<String> { async fn get_cat_ascii_art() -> color_eyre::Result<String> {
let tracer = global::tracer("");
let image_url = get_cat_image_url(client)
.with_context(Context::current_with_span(
tracer.start("get_cat_image_url"),
))
.await?;
let image_bytes = download_file(client, &image_url)
.with_context(Context::current_with_span(tracer.start("download_file")))
.await?;
let image = tracer.in_span("image::load_from_memory", |cx| {
let img = image::load_from_memory(&image_bytes)?;
cx.span()
.set_attribute(KeyValue::new("width", img.width() as i64));
cx.span()
.set_attribute(KeyValue::new("height", img.height() as i64));
Ok::<_, color_eyre::eyre::Report>(img)
})?;
let ascii_art = tracer.in_span("artem::convert", |_cx| {
artem::convert(
image,
artem::options::OptionBuilder::new()
.target(artem::options::TargetType::HtmlFile(true, true))
.build(),
)
});
Ok(ascii_art)
}
async fn get_cat_image_url(client: &reqwest::Client) -> color_eyre::Result<String> {
#[derive(Deserialize)] #[derive(Deserialize)]
struct CatImage { struct CatImage {
url: String, url: String,
} }
let api_url = "https://api.thecatapi.com/v1/images/search"; let api_url = "https://api.thecatapi.com/v1/images/search";
let client = reqwest::Client::default();
let image = client let image = client
.get(api_url) .get(api_url)
@ -153,16 +66,22 @@ async fn get_cat_image_url(client: &reqwest::Client) -> color_eyre::Result<Strin
.await? .await?
.pop() .pop()
.ok_or_else(|| color_eyre::eyre::eyre!("The Cat API returned no images"))?; .ok_or_else(|| color_eyre::eyre::eyre!("The Cat API returned no images"))?;
Ok(image.url)
}
async fn download_file(client: &reqwest::Client, url: &str) -> color_eyre::Result<Vec<u8>> { let image_bytes = client
let bytes = client .get(image.url)
.get(url)
.send() .send()
.await? .await?
.error_for_status()? .error_for_status()?
.bytes() .bytes()
.await?; .await?;
Ok(bytes.to_vec())
let image = image::load_from_memory(&image_bytes)?;
let ascii_art = artem::convert(
image,
artem::options::OptionBuilder::new()
.target(artem::options::TargetType::HtmlFile(true, true))
.build(),
);
Ok(ascii_art)
} }