Compare commits
	
		
			4 commits
		
	
	
		
			192f0e757a
			...
			aa5aaf816f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| aa5aaf816f | |||
| 460c002145 | |||
| bb32083d2c | |||
| c813356eaa | 
					 5 changed files with 833 additions and 48 deletions
				
			
		
							
								
								
									
										3
									
								
								.envrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.envrc
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| watch_file flake.nix flake.lock | ||||
| dotenv_if_exists | ||||
| use flake | ||||
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1 +1,2 @@ | |||
| target | ||||
| .env | ||||
|  |  | |||
							
								
								
									
										756
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										756
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -9,6 +9,8 @@ artem = { version = "=1.1.5", default-features = false } | |||
| axum = "0.6" | ||||
| color-eyre = "0.6" | ||||
| 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" | ||||
| reqwest = { version = "0.11", features = ["json"] } | ||||
| serde = { version = "1", features = ["derive"] } | ||||
|  |  | |||
							
								
								
									
										119
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										119
									
								
								src/main.rs
									
										
									
									
									
								
							|  | @ -2,18 +2,35 @@ use std::str::FromStr; | |||
| 
 | ||||
| use axum::{ | ||||
|     body::BoxBody, | ||||
|     http::header, | ||||
|     extract::State, | ||||
|     http::header::{self, HeaderMap}, | ||||
|     response::{IntoResponse, Response}, | ||||
|     routing::get, | ||||
|     Router, | ||||
| }; | ||||
| use opentelemetry::{ | ||||
|     global, | ||||
|     trace::{get_active_span, FutureExt, Span, Status, TraceContextExt, Tracer}, | ||||
|     Context, KeyValue, | ||||
| }; | ||||
| use reqwest::StatusCode; | ||||
| use serde::Deserialize; | ||||
| use tracing::{info, Level}; | ||||
| use tracing_subscriber::{filter::Targets, layer::SubscriberExt, util::SubscriberInitExt}; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| struct ServerState { | ||||
|     client: reqwest::Client, | ||||
| } | ||||
| 
 | ||||
| #[tokio::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")) | ||||
|         .expect("RUST_LOG should be a valid tracing filter"); | ||||
|     tracing_subscriber::fmt() | ||||
|  | @ -23,18 +40,51 @@ async fn main() { | |||
|         .with(filter) | ||||
|         .init(); | ||||
| 
 | ||||
|     let app = Router::new().route("/", get(root_get)); | ||||
|     let state = ServerState { | ||||
|         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(); | ||||
|     info!("Listening on {addr}"); | ||||
|     axum::Server::bind(&addr) | ||||
|         .serve(app.into_make_service()) | ||||
|         .with_graceful_shutdown(quit_signal) | ||||
|         .await | ||||
|         .unwrap(); | ||||
| } | ||||
| 
 | ||||
| async fn root_get() -> Response<BoxBody> { | ||||
|     match get_cat_ascii_art().await { | ||||
| async fn root_get(headers: HeaderMap, State(state): State<ServerState>) -> Response<BoxBody> { | ||||
|     let tracer = global::tracer(""); | ||||
|     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) => ( | ||||
|             StatusCode::OK, | ||||
|             [(header::CONTENT_TYPE, "text/html; charset=utf-8")], | ||||
|  | @ -42,20 +92,57 @@ async fn root_get() -> Response<BoxBody> { | |||
|         ) | ||||
|             .into_response(), | ||||
|         Err(e) => { | ||||
|             println!("Something went wrong: {e}"); | ||||
|             get_active_span(|span| { | ||||
|                 span.set_status(Status::Error { | ||||
|                     description: format!("{e}").into(), | ||||
|                 }) | ||||
|             }); | ||||
|             (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async fn get_cat_ascii_art() -> color_eyre::Result<String> { | ||||
| async fn get_cat_ascii_art(client: &reqwest::Client) -> 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)] | ||||
|     struct CatImage { | ||||
|         url: String, | ||||
|     } | ||||
| 
 | ||||
|     let api_url = "https://api.thecatapi.com/v1/images/search"; | ||||
|     let client = reqwest::Client::default(); | ||||
| 
 | ||||
|     let image = client | ||||
|         .get(api_url) | ||||
|  | @ -66,22 +153,16 @@ async fn get_cat_ascii_art() -> color_eyre::Result<String> { | |||
|         .await? | ||||
|         .pop() | ||||
|         .ok_or_else(|| color_eyre::eyre::eyre!("The Cat API returned no images"))?; | ||||
|     Ok(image.url) | ||||
| } | ||||
| 
 | ||||
|     let image_bytes = client | ||||
|         .get(image.url) | ||||
| async fn download_file(client: &reqwest::Client, url: &str) -> color_eyre::Result<Vec<u8>> { | ||||
|     let bytes = client | ||||
|         .get(url) | ||||
|         .send() | ||||
|         .await? | ||||
|         .error_for_status()? | ||||
|         .bytes() | ||||
|         .await?; | ||||
| 
 | ||||
|     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) | ||||
|     Ok(bytes.to_vec()) | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue