diff --git a/Cargo.lock b/Cargo.lock index 15f88f7..bd3f7ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -561,6 +561,9 @@ dependencies = [ "sqlx", "time", "tokio", + "tower-http", + "tracing", + "tracing-subscriber", "twilight-http", "twilight-interactions", "twilight-mention", @@ -833,6 +836,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.7.0" @@ -915,6 +927,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -994,6 +1016,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "packed_simd_2" version = "0.3.8" @@ -1192,6 +1220,30 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + [[package]] name = "ring" version = "0.16.20" @@ -1409,6 +1461,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1638,6 +1699,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.20" @@ -1790,6 +1861,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1835,6 +1907,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2018,6 +2120,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 0a19957..b839e4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ sqlx = { version = "0.6.2", features = [ ]} time = "0.3.20" tokio = { version = "1.26.0", features = ["full"] } +tower-http = { version = "0.4.0", features = ["trace"] } +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } twilight-http = "0.15.1" twilight-interactions = "0.15.0" twilight-mention = "0.15.1" diff --git a/src/main.rs b/src/main.rs index d4b3eee..927e8b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,40 +7,53 @@ use axum::{ Json, Router, }; use base64::{alphabet, engine, Engine}; -use commands::{SetFactCommand, GetFactCommand, set_fact, get_fact}; +use commands::{get_fact, set_fact, GetFactCommand, SetFactCommand}; use ed25519_dalek::{Signature, VerifyingKey}; use serde::Deserialize; use sqlx::{postgres::PgPoolOptions, PgPool}; +use tower_http::trace::TraceLayer; +use tracing::{Instrument, debug_span}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use twilight_http::Client; use twilight_interactions::command::{CommandInputData, CommandModel, CreateCommand}; use twilight_model::{ application::interaction::{Interaction, InteractionData, InteractionType}, http::interaction::{InteractionResponse, InteractionResponseType}, - id::Id + id::Id, }; mod commands; mod database; #[tokio::main] async fn main() -> anyhow::Result<()> { - let port = 4635; dotenvy::dotenv().ok(); + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "god_replacement_product=debug,tower_http=debug".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let port = listen_port()?; + let pg_pool = PgPoolOptions::new() .max_connections(5) - .connect(database_url().as_str()) + .connect(database_url()?.as_str()) .await?; sqlx::migrate!().run(&pg_pool).await?; let app = Router::new() .route("/", post(post_interaction)) - .with_state(pg_pool); + .with_state(pg_pool) + .layer(TraceLayer::new_for_http()); + register_commands().await?; let addr = SocketAddr::from(([127, 0, 0, 1], port)); - - register_commands().await; + tracing::debug!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) @@ -94,6 +107,7 @@ async fn post_interaction( kind: InteractionResponseType::Pong, data: None, }; + Ok((StatusCode::OK, Json(pong))) } InteractionType::ApplicationCommand => { @@ -102,7 +116,11 @@ async fn post_interaction( return not_found(); }; let command_input_data = CommandInputData::from(*data.clone()); - match &*data.name { + let slash_command_span = debug_span!("discord_slash_command", name=data.name.to_owned()); + slash_command_span.in_scope(|| { + tracing::debug!("started processing command"); + }); + let result = match &*data.name { SetFactCommand::NAME => { let Ok(command_data) = SetFactCommand::from_interaction(command_input_data) else { return Err((StatusCode::BAD_REQUEST, format!("invalid {0} command.", SetFactCommand::NAME))); @@ -110,11 +128,20 @@ async fn post_interaction( let Some(author_id) = author_id else { return Err((StatusCode::BAD_REQUEST, format!("{0} requires a user.", SetFactCommand::NAME))); }; - match set_fact(interaction.id, interaction.channel_id, author_id, command_data, &pg_pool).await { + match set_fact( + interaction.id, + interaction.channel_id, + author_id, + command_data, + &pg_pool, + ) + .instrument(slash_command_span.clone()) + .await + { Ok(response) => Ok((StatusCode::OK, Json(response))), Err(err) => Err(err), } - }, + } GetFactCommand::NAME => { let Ok(command_data) = GetFactCommand::from_interaction(command_input_data) else { return Err((StatusCode::BAD_REQUEST, format!("invalid {0} command.", GetFactCommand::NAME))); @@ -122,14 +149,21 @@ async fn post_interaction( let Some(author_id) = author_id else { return Err((StatusCode::BAD_REQUEST, format!("{0} requires a user.", GetFactCommand::NAME))); }; - - match get_fact(interaction.channel_id, author_id, command_data, &pg_pool).await { + + match get_fact(interaction.channel_id, author_id, command_data, &pg_pool).instrument(slash_command_span.clone()).await + { Ok(response) => Ok((StatusCode::OK, Json(response))), Err(err) => Err(err), } - }, + } _ => not_found(), - } + }; + + slash_command_span.in_scope(|| { + tracing::debug!("finished processing command"); + }); + + result } _ => not_found(), } @@ -151,15 +185,15 @@ fn discord_pub_key() -> VerifyingKey { VerifyingKey::from_bytes(&pub_key_bytes).unwrap() } -async fn register_commands() { - discord_client() - .interaction(Id::from_str(&discord_client_id()).unwrap()) +async fn register_commands() -> anyhow::Result<()> { + discord_client()? + .interaction(Id::from_str(&discord_client_id()?)?) .set_global_commands(&[ GetFactCommand::create_command().into(), SetFactCommand::create_command().into(), - ]) - .await - .unwrap(); + ]) + .await?; + Ok(()) } #[derive(Deserialize)] @@ -167,37 +201,42 @@ struct ClientCredentialsResponse { access_token: String, } -fn authorization() -> String { +fn authorization() -> anyhow::Result { let engine = engine::GeneralPurpose::new(&alphabet::STANDARD, engine::general_purpose::PAD); - let auth = format!("{}:{}", discord_client_id(), discord_client_secret(),); - engine.encode(auth) + let auth = format!("{}:{}", discord_client_id()?, discord_client_secret()?); + Ok(engine.encode(auth)) } -fn client_credentials_grant() -> ClientCredentialsResponse { - ureq::post("https://discord.com/api/v10/oauth2/token") - .set("Authorization", &format!("Basic {}", authorization())) +fn client_credentials_grant() -> anyhow::Result { + Ok(ureq::post("https://discord.com/api/v10/oauth2/token") + .set("Authorization", &format!("Basic {}", authorization()?)) .send_form(&[ ("grant_type", "client_credentials"), ("scope", "applications.commands.update"), ]) .unwrap() - .into_json() - .unwrap() + .into_json()?) } -fn discord_client_id() -> String { - std::env::var("DISCORD_CLIENT_ID").unwrap() +fn discord_client_id() -> anyhow::Result { + std::env::var("DISCORD_CLIENT_ID").map_err(Into::into) } -fn discord_client_secret() -> String { - std::env::var("DISCORD_CLIENT_SECRET").unwrap() +fn discord_client_secret() -> anyhow::Result { + std::env::var("DISCORD_CLIENT_SECRET").map_err(Into::into) } -fn discord_client() -> Client { - let token = client_credentials_grant().access_token; - Client::new(format!("Bearer {token}")) +fn discord_client() -> anyhow::Result { + let token = client_credentials_grant()?.access_token; + Ok(Client::new(format!("Bearer {token}"))) } -fn database_url() -> String { - std::env::var("DATABASE_URL").unwrap() +fn database_url() -> anyhow::Result { + std::env::var("DATABASE_URL").map_err(Into::into) +} + +fn listen_port() -> anyhow::Result { + std::env::var("LISTEN_PORT") + .map_err(Into::into) + .and_then(|v| v.parse::().map_err(Into::into)) }