diff --git a/Cargo.lock b/Cargo.lock index 1c81115..93cb391 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -943,14 +943,13 @@ dependencies = [ "dirs", "dotenv", "hyperlog-core", + "hyperlog-server", "hyperlog-tui", "serde", "serde_json", "similar-asserts", - "sqlx", "tempfile", "tokio", - "tower-http", "tracing", "tracing-subscriber", "uuid", @@ -978,6 +977,24 @@ dependencies = [ "uuid", ] +[[package]] +name = "hyperlog-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "hyperlog-core", + "serde", + "serde_json", + "similar-asserts", + "sqlx", + "tempfile", + "tokio", + "tower-http", + "tracing", + "uuid", +] + [[package]] name = "hyperlog-tui" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f20725d..7065027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ resolver = "2" [workspace.dependencies] hyperlog-core = { path = "crates/hyperlog-core" } hyperlog-tui = { path = "crates/hyperlog-tui" } +hyperlog-server = { path = "crates/hyperlog-server" } anyhow = { version = "1" } tokio = { version = "1", features = ["full"] } @@ -13,8 +14,10 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } clap = { version = "4", features = ["derive", "env"] } dotenv = { version = "0.15" } axum = { version = "0.7" } +serde = { version = "1.0.201", features = ["derive"] } serde_json = "1.0.117" itertools = "0.12.1" +uuid = { version = "1.8.0", features = ["v4"] } [workspace.package] version = "0.1.0" diff --git a/crates/hyperlog-server/Cargo.toml b/crates/hyperlog-server/Cargo.toml new file mode 100644 index 0000000..8991522 --- /dev/null +++ b/crates/hyperlog-server/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "hyperlog-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +hyperlog-core.workspace = true + +anyhow.workspace = true +tokio.workspace = true +tracing.workspace = true +axum.workspace = true +serde.workspace = true +serde_json.workspace = true +uuid.workspace = true + +tower-http = { version = "0.5.2", features = ["cors", "trace"] } +sqlx = { version = "0.7.4", features = [ + "runtime-tokio", + "tls-rustls", + "postgres", + "uuid", + "time", +] } + +[dev-dependencies] +similar-asserts = "1.5.0" +tempfile = "3.10.1" diff --git a/crates/hyperlog/migrations/crdb/20240201211013_initial.sql b/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql similarity index 100% rename from crates/hyperlog/migrations/crdb/20240201211013_initial.sql rename to crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql diff --git a/crates/hyperlog-server/src/external_http.rs b/crates/hyperlog-server/src/external_http.rs new file mode 100644 index 0000000..2dbe14e --- /dev/null +++ b/crates/hyperlog-server/src/external_http.rs @@ -0,0 +1,40 @@ +use std::{net::SocketAddr, sync::Arc}; + +use axum::{extract::MatchedPath, http::Request, routing::get, Router}; +use tower_http::trace::TraceLayer; + +use crate::state::{SharedState, State}; + +async fn root() -> &'static str { + "Hello, hyperlog!" +} + +pub async fn serve(state: &SharedState, host: &SocketAddr) -> anyhow::Result<()> { + let app = Router::new() + .route("/", get(root)) + .with_state(state.clone()) + .layer( + TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { + // Log the matched route's path (with placeholders not filled in). + // Use request.uri() or OriginalUri if you want the real path. + let matched_path = request + .extensions() + .get::() + .map(MatchedPath::as_str); + + tracing::info_span!( + "http_request", + method = ?request.method(), + matched_path, + some_other_field = tracing::field::Empty, + ) + }), // ... + ); + + tracing::info!("listening on {}", host); + let listener = tokio::net::TcpListener::bind(host).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); + Ok(()) +} diff --git a/crates/hyperlog-server/src/internal_http.rs b/crates/hyperlog-server/src/internal_http.rs new file mode 100644 index 0000000..2dbe14e --- /dev/null +++ b/crates/hyperlog-server/src/internal_http.rs @@ -0,0 +1,40 @@ +use std::{net::SocketAddr, sync::Arc}; + +use axum::{extract::MatchedPath, http::Request, routing::get, Router}; +use tower_http::trace::TraceLayer; + +use crate::state::{SharedState, State}; + +async fn root() -> &'static str { + "Hello, hyperlog!" +} + +pub async fn serve(state: &SharedState, host: &SocketAddr) -> anyhow::Result<()> { + let app = Router::new() + .route("/", get(root)) + .with_state(state.clone()) + .layer( + TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { + // Log the matched route's path (with placeholders not filled in). + // Use request.uri() or OriginalUri if you want the real path. + let matched_path = request + .extensions() + .get::() + .map(MatchedPath::as_str); + + tracing::info_span!( + "http_request", + method = ?request.method(), + matched_path, + some_other_field = tracing::field::Empty, + ) + }), // ... + ); + + tracing::info!("listening on {}", host); + let listener = tokio::net::TcpListener::bind(host).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); + Ok(()) +} diff --git a/crates/hyperlog-server/src/lib.rs b/crates/hyperlog-server/src/lib.rs new file mode 100644 index 0000000..003b460 --- /dev/null +++ b/crates/hyperlog-server/src/lib.rs @@ -0,0 +1,36 @@ +use std::{net::SocketAddr, sync::Arc}; + +use crate::state::{SharedState, State}; + +mod external_http; +mod internal_http; +mod state; + +#[derive(Clone)] +pub struct ServeOptions { + pub external_http: SocketAddr, + pub internal_http: SocketAddr, +} + +pub async fn serve(opts: ServeOptions) -> anyhow::Result<()> { + let ctrl_c = async { + tokio::signal::ctrl_c().await.unwrap(); + tracing::info!("kill signal received, shutting down"); + }; + tracing::debug!("setting up dependencies"); + let state = SharedState(Arc::new(State::new().await?)); + + tracing::debug!("serve starting"); + tokio::select!( + res = external_http::serve(&state, &opts.external_http) => { + res? + }, + res = internal_http::serve(&state, &opts.internal_http) => { + res? + }, + () = ctrl_c => {} + ); + tracing::debug!("serve finalized"); + + Ok(()) +} diff --git a/crates/hyperlog-server/src/state.rs b/crates/hyperlog-server/src/state.rs new file mode 100644 index 0000000..68a7309 --- /dev/null +++ b/crates/hyperlog-server/src/state.rs @@ -0,0 +1,37 @@ +use std::{ops::Deref, sync::Arc}; + +use anyhow::Context; +use sqlx::{Pool, Postgres}; + +#[derive(Clone)] +pub struct SharedState(pub Arc); + +impl Deref for SharedState { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct State { + pub _db: Pool, +} + +impl State { + pub async fn new() -> anyhow::Result { + let db = sqlx::PgPool::connect( + &std::env::var("DATABASE_URL").context("DATABASE_URL is not set")?, + ) + .await?; + + sqlx::migrate!("migrations/crdb") + .set_locking(false) + .run(&db) + .await?; + + let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?; + + Ok(Self { _db: db }) + } +} diff --git a/crates/hyperlog/Cargo.toml b/crates/hyperlog/Cargo.toml index 5454c7a..0c60d19 100644 --- a/crates/hyperlog/Cargo.toml +++ b/crates/hyperlog/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://git.front.kjuulh.io/kjuulh/hyperlog" [dependencies] hyperlog-core.workspace = true hyperlog-tui.workspace = true +hyperlog-server = { workspace = true, optional = true } anyhow.workspace = true tokio.workspace = true @@ -15,21 +16,17 @@ tracing-subscriber.workspace = true clap.workspace = true dotenv.workspace = true axum.workspace = true +serde.workspace = true serde_json.workspace = true +uuid.workspace = true -serde = { version = "1.0.201", features = ["derive"] } -sqlx = { version = "0.7.4", features = [ - "runtime-tokio", - "tls-rustls", - "postgres", - "uuid", - "time", -] } -uuid = { version = "1.8.0", features = ["v4"] } -tower-http = { version = "0.5.2", features = ["cors", "trace"] } bus = "2.4.1" dirs = "5.0.1" [dev-dependencies] similar-asserts = "1.5.0" tempfile = "3.10.1" + +[features] +default = [] +include_server = ["dep:hyperlog-server"] diff --git a/crates/hyperlog/src/cli.rs b/crates/hyperlog/src/cli.rs index 55d9369..94ea478 100644 --- a/crates/hyperlog/src/cli.rs +++ b/crates/hyperlog/src/cli.rs @@ -3,8 +3,6 @@ use std::net::SocketAddr; use clap::{Parser, Subcommand}; use hyperlog_core::{commander, state}; -use crate::server::serve; - #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Command { @@ -14,9 +12,14 @@ struct Command { #[derive(Subcommand)] enum Commands { + #[cfg(feature = "include_server")] Serve { - #[arg(env = "SERVICE_HOST", long, default_value = "127.0.0.1:3000")] - host: SocketAddr, + #[arg(env = "EXTERNAL_HOST", long, default_value = "127.0.0.1:3000")] + external_host: SocketAddr, + #[arg(env = "INTERNAL_HOST", long, default_value = "127.0.0.1:3001")] + internal_host: SocketAddr, + #[arg(env = "EXTERNAL_GRPC_HOST", long, default_value = "127.0.0.1:4000")] + external_grpc_host: SocketAddr, }, Exec { #[command(subcommand)] @@ -70,58 +73,75 @@ pub async fn execute() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); } - let state = state::State::new()?; - match cli.command { - Some(Commands::Serve { host }) => { + #[cfg(feature = "include_server")] + Some(Commands::Serve { + external_host, + internal_host, + .. + }) => { tracing::info!("Starting service"); - serve(host).await?; + hyperlog_server::serve(hyperlog_server::ServeOptions { + external_http: external_host, + internal_http: internal_host, + }) + .await?; } - Some(Commands::Exec { commands }) => match commands { - ExecCommands::CreateRoot { root } => state - .commander - .execute(commander::Command::CreateRoot { root })?, - ExecCommands::CreateSection { root, path } => { - state.commander.execute(commander::Command::CreateSection { - root, - path: path - .unwrap_or_default() - .split('.') - .map(|s| s.to_string()) - .filter(|s| !s.is_empty()) - .collect::>(), - })? + Some(Commands::Exec { commands }) => { + let state = state::State::new()?; + match commands { + ExecCommands::CreateRoot { root } => state + .commander + .execute(commander::Command::CreateRoot { root })?, + ExecCommands::CreateSection { root, path } => { + state.commander.execute(commander::Command::CreateSection { + root, + path: path + .unwrap_or_default() + .split('.') + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect::>(), + })? + } } - }, - Some(Commands::Query { commands }) => match commands { - QueryCommands::Get { root, path } => { - let res = state.querier.get( - &root, - path.unwrap_or_default() - .split('.') - .filter(|s| !s.is_empty()), - ); + } + Some(Commands::Query { commands }) => { + let state = state::State::new()?; + match commands { + QueryCommands::Get { root, path } => { + let res = state.querier.get( + &root, + path.unwrap_or_default() + .split('.') + .filter(|s| !s.is_empty()), + ); - let output = serde_json::to_string_pretty(&res)?; + let output = serde_json::to_string_pretty(&res)?; - println!("{}", output); + println!("{}", output); + } } - }, + } Some(Commands::CreateRoot { name }) => { + let state = state::State::new()?; state .commander .execute(commander::Command::CreateRoot { root: name })?; println!("Root was successfully created, now run:\n\n$ hyperlog"); } Some(Commands::Info {}) => { + let state = state::State::new()?; println!("graph stored at: {}", state.storage.info()?) } Some(Commands::ClearLock {}) => { + let state = state::State::new()?; state.storage.clear_lock_file(); println!("cleared lock file"); } None => { + let state = state::State::new()?; hyperlog_tui::execute(state).await?; } } diff --git a/crates/hyperlog/src/main.rs b/crates/hyperlog/src/main.rs index adad57b..fc41488 100644 --- a/crates/hyperlog/src/main.rs +++ b/crates/hyperlog/src/main.rs @@ -1,6 +1,4 @@ mod cli; -pub(crate) mod server; -pub(crate) mod state; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/crates/hyperlog/src/server.rs b/crates/hyperlog/src/server.rs index f734d1c..8b13789 100644 --- a/crates/hyperlog/src/server.rs +++ b/crates/hyperlog/src/server.rs @@ -1,42 +1 @@ -use std::{net::SocketAddr, sync::Arc}; -use axum::{extract::MatchedPath, http::Request, routing::get, Router}; -use tower_http::trace::TraceLayer; - -use crate::state::{SharedState, State}; - -async fn root() -> &'static str { - "Hello, hyperlog!" -} - -pub async fn serve(host: SocketAddr) -> anyhow::Result<()> { - let state = SharedState(Arc::new(State::new().await?)); - - let app = Router::new() - .route("/", get(root)) - .with_state(state.clone()) - .layer( - TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { - // Log the matched route's path (with placeholders not filled in). - // Use request.uri() or OriginalUri if you want the real path. - let matched_path = request - .extensions() - .get::() - .map(MatchedPath::as_str); - - tracing::info_span!( - "http_request", - method = ?request.method(), - matched_path, - some_other_field = tracing::field::Empty, - ) - }), // ... - ); - - tracing::info!("listening on {}", host); - let listener = tokio::net::TcpListener::bind(host).await.unwrap(); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); - Ok(()) -} diff --git a/crates/hyperlog/src/state.rs b/crates/hyperlog/src/state.rs index 68a7309..8b13789 100644 --- a/crates/hyperlog/src/state.rs +++ b/crates/hyperlog/src/state.rs @@ -1,37 +1 @@ -use std::{ops::Deref, sync::Arc}; -use anyhow::Context; -use sqlx::{Pool, Postgres}; - -#[derive(Clone)] -pub struct SharedState(pub Arc); - -impl Deref for SharedState { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct State { - pub _db: Pool, -} - -impl State { - pub async fn new() -> anyhow::Result { - let db = sqlx::PgPool::connect( - &std::env::var("DATABASE_URL").context("DATABASE_URL is not set")?, - ) - .await?; - - sqlx::migrate!("migrations/crdb") - .set_locking(false) - .run(&db) - .await?; - - let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?; - - Ok(Self { _db: db }) - } -} diff --git a/cuddle.yaml b/cuddle.yaml index 561c295..2f57133 100644 --- a/cuddle.yaml +++ b/cuddle.yaml @@ -15,3 +15,7 @@ please: api_url: https://git.front.kjuulh.io actions: rust: + +scripts: + dev: + type: shell diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..81e6e60 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env zsh + +echo "starting services" +docker compose -f templates/docker-compose.yaml up -d --remove-orphans + +tear_down() { + docker compose -f templates/docker-compose.yaml down -v || true +} + +trap tear_down EXIT + +RUST_LOG=trace,tokio=info,tower=info,mio=info,sqlx=info cargo run -F include_server -- serve