diff --git a/.github/workflows/backend_general.yml b/.github/workflows/backend_general.yml index 65d23db7..ff2fdeb7 100644 --- a/.github/workflows/backend_general.yml +++ b/.github/workflows/backend_general.yml @@ -68,11 +68,11 @@ jobs: - name: Migrate database run: | sudo apt-get install libpq-dev -y - SKIP_DOCKER=true POSTGRES_PORT=5433 ./scripts/init_database.sh + SKIP_DOCKER=true POSTGRES_PORT=5433 ./build/init_database.sh - name: Check sqlx-data.json is up-to-date run: | - cargo sqlx prepare --check -- --bin http_server + cargo sqlx prepare --check -- --bin appflowy_server - name: Run cargo test run: cargo test @@ -140,7 +140,7 @@ jobs: - name: Migrate database run: | sudo apt-get install libpq-dev -y - SKIP_DOCKER=true POSTGRES_PORT=5433 ./scripts/init_database.sh + SKIP_DOCKER=true POSTGRES_PORT=5433 ./build/init_database.sh - run: rustup component add clippy - run: cargo clippy -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index a8cc3178..9a7a4fef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,9 +401,9 @@ dependencies = [ "sqlx", "tokio", "tracing", - "tracing-appender", + "tracing-actix-web", "tracing-bunyan-formatter", - "tracing-futures", + "tracing-log", "tracing-subscriber", ] @@ -2137,7 +2137,7 @@ dependencies = [ "thiserror", "tokio-stream", "url", - "uuid", + "uuid 0.8.2", "whoami", ] @@ -2388,14 +2388,15 @@ dependencies = [ ] [[package]] -name = "tracing-appender" -version = "0.2.2" +name = "tracing-actix-web" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +checksum = "4082e4d81173e0b7ad3cfb71e9eaef0dd0cbb7b139fdb56394f488a3b0760b23" dependencies = [ - "crossbeam-channel", - "time 0.3.20", - "tracing-subscriber", + "actix-web", + "pin-project", + "tracing", + "uuid 1.3.0", ] [[package]] @@ -2437,16 +2438,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" version = "0.1.3" @@ -2567,6 +2558,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +[[package]] +name = "uuid" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +dependencies = [ + "getrandom", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 613530bd..bf50b5f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,10 +36,10 @@ derive_more = {version = "0.99"} # tracing tracing = { version = "0.1.37" } -tracing-futures = "0.2.5" tracing-subscriber = { version = "0.3.16", features = ["registry", "env-filter", "ansi", "json"] } tracing-bunyan-formatter = "0.3.6" -tracing-appender = "0.2.2" +tracing-actix-web = "0.7" +tracing-log = "0.1.1" ormx = { version = "0.10.0", features = ["postgres"]} [dependencies.sqlx] @@ -56,7 +56,7 @@ features = [ ] [[bin]] -name = "http_server" +name = "appflowy_server" path = "src/main.rs" diff --git a/Dockerfile b/Dockerfile index bbc032cc..39c7d3fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,11 +2,11 @@ FROM rust:1.56.1 as builder WORKDIR /app COPY . . -WORKDIR /app/services/http_server +WORKDIR /app/services/appflowy_server ENV SQLX_OFFLINE true -RUN RUSTFLAGS="-C opt-level=2" cargo build --release --bin http_server +RUN RUSTFLAGS="-C opt-level=2" cargo build --release --bin appflowy_server # Size optimization -#RUN strip ./target/release/http_server +#RUN strip ./target/release/appflowy_server FROM debian:bullseye-slim AS runtime WORKDIR /app @@ -17,7 +17,7 @@ RUN apt-get update -y \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/services/target/release/http_server /usr/local/bin/http_server -COPY --from=builder /app/services/http_server/configuration configuration +COPY --from=builder /app/services/target/release/appflowy_server /usr/local/bin/appflowy_server +COPY --from=builder /app/services/appflowy_server/configuration configuration ENV APP_ENVIRONMENT production -CMD ["http_server"] +CMD ["appflowy_server"] diff --git a/Makefile b/Makefile index de0be1f6..37aa6231 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -ROOT = "./scripts" +ROOT = "./build" SEMVER_VERSION=$(shell grep version Cargo.toml | awk -F"\"" '{print $$2}' | head -n 1) .PHONY: init_database docker_image local_server docker_test @@ -8,7 +8,7 @@ init_database: docker_image: source $(ROOT)/docker_env.sh && docker-compose up -d postgres_db - source $(ROOT)/docker_env.sh && docker-compose up -d http_server + source $(ROOT)/docker_env.sh && docker-compose up -d appflowy_server local_server: cargo run diff --git a/docker-compose.yml b/docker-compose.yml index 183e6730..a5ca5d57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,15 +8,15 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} ports: - "5434:5432" - http_server: + appflowy_server: restart: on-failure environment: - APP_ENVIRONMENT=production - DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${POSTGRES_DB}" build: context: ../../ - dockerfile: ./services/http_server/Dockerfile - image: http_server:${BACKEND_VERSION} + dockerfile: ./services/appflowy_server/Dockerfile + image: appflowy_server:${BACKEND_VERSION} depends_on: - postgres_db ports: diff --git a/src/application.rs b/src/application.rs index 778835eb..70073a57 100644 --- a/src/application.rs +++ b/src/application.rs @@ -3,10 +3,11 @@ use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer}; use sqlx::{postgres::PgPoolOptions, PgPool}; use std::net::TcpListener; use std::sync::Arc; +use tracing_actix_web::TracingLogger; use crate::api::{token_scope, user_scope}; -use crate::config::config::{Config, DatabaseSettings}; +use crate::config::config::{Config, DatabaseSetting}; use crate::config::env::{domain, secret}; use crate::middleware::cors::default_cors; use crate::state::State; @@ -46,6 +47,7 @@ pub fn run(listener: TcpListener, state: State) -> Result State { } } -pub async fn get_connection_pool(setting: &DatabaseSettings) -> Result { +pub async fn get_connection_pool(setting: &DatabaseSetting) -> Result { PgPoolOptions::new() .connect_timeout(std::time::Duration::from_secs(5)) .connect_with(setting.with_db()) diff --git a/src/component/log.rs b/src/component/log.rs deleted file mode 100644 index 67154e62..00000000 --- a/src/component/log.rs +++ /dev/null @@ -1,22 +0,0 @@ -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::EnvFilter; - -pub fn init_log() { - if std::env::var_os("RUST_LOG").is_none() { - std::env::set_var("RUST_LOG", "debug"); - } - let subscriber = tracing_subscriber::fmt() - .with_target(true) - .with_max_level(tracing::Level::TRACE) - .with_writer(std::io::stderr) - .with_thread_ids(true) - .compact() - .finish() - .with(EnvFilter::from_default_env()); - - // let formatting_layer = BunyanFormattingLayer::new(self.name, std::io::stdout); - // set_global_default(subscriber.with(JsonStorageLayer).with(formatting_layer)).map_err(|e| format!("{:?}", e))?; - - subscriber.try_init().unwrap() -} diff --git a/src/component/mod.rs b/src/component/mod.rs index f4ee9bc8..8b137891 100644 --- a/src/component/mod.rs +++ b/src/component/mod.rs @@ -1 +1 @@ -pub mod log; + diff --git a/src/config/config.rs b/src/config/config.rs index 37113afc..05337012 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -5,7 +5,7 @@ use std::convert::{TryFrom, TryInto}; #[derive(serde::Deserialize, Clone, Debug)] pub struct Config { - pub database: DatabaseSettings, + pub database: DatabaseSetting, pub application: ApplicationSettings, } @@ -25,7 +25,7 @@ pub struct ApplicationSettings { } #[derive(serde::Deserialize, Clone, Debug)] -pub struct DatabaseSettings { +pub struct DatabaseSetting { pub username: String, pub password: String, #[serde(deserialize_with = "deserialize_number_from_string")] @@ -35,7 +35,7 @@ pub struct DatabaseSettings { pub require_ssl: bool, } -impl DatabaseSettings { +impl DatabaseSetting { pub fn without_db(&self) -> PgConnectOptions { let ssl_mode = if self.require_ssl { PgSslMode::Require diff --git a/src/lib.rs b/src/lib.rs index 270d604f..c8ac1c1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,3 +4,4 @@ pub mod component; pub mod config; pub mod middleware; pub mod state; +pub mod telemetry; diff --git a/src/main.rs b/src/main.rs index be344e17..83bbfcc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ use appflowy_server::application::{init_state, Application}; -use appflowy_server::component::log::init_log; use appflowy_server::config::config::get_configuration; +use appflowy_server::telemetry::{get_subscriber, init_subscriber}; #[actix_web::main] async fn main() -> std::io::Result<()> { - init_log(); + let subscriber = get_subscriber("appflowy_server".into(), "info".into(), std::io::stdout); + init_subscriber(subscriber); + let configuration = get_configuration().expect("Failed to read configuration."); let state = init_state(&configuration).await; let application = Application::build(configuration, state).await?; diff --git a/src/telemetry.rs b/src/telemetry.rs new file mode 100644 index 00000000..076288ce --- /dev/null +++ b/src/telemetry.rs @@ -0,0 +1,42 @@ +use actix_web::rt::task::JoinHandle; +use tracing::subscriber::set_global_default; +use tracing::Subscriber; +use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; +use tracing_log::LogTracer; +use tracing_subscriber::fmt::MakeWriter; +use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; + +/// Compose multiple layers into a `tracing`'s subscriber. +pub fn get_subscriber( + name: String, + env_filter: String, + sink: Sink, +) -> impl Subscriber + Sync + Send +where + Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static, +{ + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter)); + let formatting_layer = BunyanFormattingLayer::new(name, sink); + Registry::default() + .with(env_filter) + .with(JsonStorageLayer) + .with(formatting_layer) +} + +/// Register a subscriber as global default to process span data. +/// +/// It should only be called once! +pub fn init_subscriber(subscriber: impl Subscriber + Sync + Send) { + LogTracer::init().expect("Failed to set logger"); + set_global_default(subscriber).expect("Failed to set subscriber"); +} + +pub fn spawn_blocking_with_tracing(f: F) -> JoinHandle +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + let current_span = tracing::Span::current(); + actix_web::rt::task::spawn_blocking(move || current_span.in_scope(f)) +} diff --git a/tests/api/main.rs b/tests/api/main.rs new file mode 100644 index 00000000..3479fd59 --- /dev/null +++ b/tests/api/main.rs @@ -0,0 +1 @@ +mod test_server; diff --git a/tests/api/test_server.rs b/tests/api/test_server.rs new file mode 100644 index 00000000..d8202763 --- /dev/null +++ b/tests/api/test_server.rs @@ -0,0 +1,56 @@ +use appflowy_server::application::{init_state, Application}; +use appflowy_server::config::config::{get_configuration, DatabaseSetting}; +use appflowy_server::state::State; +use sqlx::types::Uuid; +use sqlx::{Connection, Executor, PgConnection, PgPool}; + +#[derive(Clone)] +pub struct TestServer { + pub state: State, +} + +pub async fn spawn_server() -> TestServer { + let database_name = Uuid::new_v4().to_string(); + let configuration = { + let mut c = get_configuration().expect("Failed to read configuration."); + c.database.database_name = database_name.clone(); + // Use a random OS port + c.application.port = 0; + c + }; + + let _ = configure_database(&configuration.database).await; + let state = init_state(&configuration).await; + let application = Application::build(configuration.clone(), state.clone()) + .await + .expect("Failed to build application."); + + let _ = tokio::spawn(async { + let _ = application.run_until_stopped(); + }); + + TestServer { state } +} + +async fn configure_database(config: &DatabaseSetting) -> PgPool { + // Create database + let mut connection = PgConnection::connect_with(&config.without_db()) + .await + .expect("Failed to connect to Postgres"); + connection + .execute(&*format!(r#"CREATE DATABASE "{}";"#, config.database_name)) + .await + .expect("Failed to create database."); + + // Migrate database + let connection_pool = PgPool::connect_with(config.with_db()) + .await + .expect("Failed to connect to Postgres."); + + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .expect("Failed to migrate the database"); + + connection_pool +}