Telemetry
This commit is contained in:
5
.github/workflows/general.yml
vendored
5
.github/workflows/general.yml
vendored
@@ -53,12 +53,10 @@ jobs:
|
||||
# This is an action that checks out your repository onto the runner, allowing you to run scripts or other actions against your code (such as build and test tools).
|
||||
# You should use the checkout action any time your workflow will run against the repository's code.
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# This GitHub Action installs a Rust toolchain using rustup. It is designed for one-line concise usage and good defaults.
|
||||
# It also takes care of caching intermediate build artifacts.
|
||||
- name: Install the Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install sqlx-cli
|
||||
run: cargo install sqlx-cli
|
||||
--version=${{ env.SQLX_VERSION }}
|
||||
@@ -76,13 +74,10 @@ jobs:
|
||||
# Grant create db privileges to the app user
|
||||
GRANT_QUERY="ALTER USER ${APP_USER} CREATEDB;"
|
||||
PGPASSWORD="password" psql -U "postgres" -h "localhost" -c "${GRANT_QUERY}"
|
||||
|
||||
- name: Migrate database
|
||||
run: SKIP_DOCKER=true ./scripts/init_db.sh
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test
|
||||
|
||||
- name: Check that queries are fresh
|
||||
run: cargo sqlx prepare --workspace --check -- --all-targets
|
||||
|
||||
|
||||
17
.sqlx/query-829555ccf8877b594f4b0691e42315405521c985dfd196902df2ccc74e2aeb49.json
generated
Normal file
17
.sqlx/query-829555ccf8877b594f4b0691e42315405521c985dfd196902df2ccc74e2aeb49.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO subscriptions (id, email, name, subscribed_at)\n VALUES ($1, $2, $3, $4);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "829555ccf8877b594f4b0691e42315405521c985dfd196902df2ccc74e2aeb49"
|
||||
}
|
||||
119
Cargo.lock
generated
119
Cargo.lock
generated
@@ -17,6 +17,19 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -381,6 +394,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -620,6 +642,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
@@ -1226,6 +1258,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@@ -1473,6 +1511,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -1766,6 +1810,16 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "secrecy"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
@@ -2269,6 +2323,37 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
@@ -2477,6 +2562,24 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-bunyan-formatter"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d637245a0d8774bd48df6482e086c59a8b5348a910c3b0579354045a9d82411"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"gethostname",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log 0.1.4",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
@@ -2487,6 +2590,17 @@ dependencies = [
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
@@ -2513,7 +2627,7 @@ dependencies = [
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-log 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3076,12 +3190,15 @@ dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"config",
|
||||
"once_cell",
|
||||
"reqwest",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-bunyan-formatter",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -14,13 +14,16 @@ name = "zero2prod"
|
||||
axum = "0.8.4"
|
||||
chrono = { version = "0.4.41", default-features = false, features = ["clock"] }
|
||||
config = "0.15.14"
|
||||
secrecy = { version = "0.10.3", features = ["serde"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "macros", "postgres", "uuid", "chrono", "migrate"] }
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.6.6", features = ["trace"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-bunyan-formatter = "0.3.10"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
uuid = { version = "1.18.0", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
once_cell = "1.21.3"
|
||||
reqwest = "0.12.23"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
|
||||
@@ -19,24 +20,33 @@ pub struct Settings {
|
||||
#[derive(Deserialize)]
|
||||
pub struct DatabaseSettings {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub password: SecretString,
|
||||
pub port: u16,
|
||||
pub host: String,
|
||||
pub database_name: String,
|
||||
}
|
||||
|
||||
impl DatabaseSettings {
|
||||
pub fn connection_string(&self) -> String {
|
||||
pub fn connection_string(&self) -> SecretString {
|
||||
format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
self.username, self.password, self.host, self.port, self.database_name
|
||||
self.username,
|
||||
self.password.expose_secret(),
|
||||
self.host,
|
||||
self.port,
|
||||
self.database_name
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn connection_string_without_db(&self) -> String {
|
||||
pub fn connection_string_without_db(&self) -> SecretString {
|
||||
format!(
|
||||
"postgres://{}:{}@{}:{}",
|
||||
self.username, self.password, self.host, self.port
|
||||
self.username,
|
||||
self.password.expose_secret(),
|
||||
self.host,
|
||||
self.port
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod configuration;
|
||||
pub mod routes;
|
||||
pub mod startup;
|
||||
pub mod telemetry;
|
||||
|
||||
21
src/main.rs
21
src/main.rs
@@ -1,29 +1,18 @@
|
||||
use secrecy::ExposeSecret;
|
||||
use sqlx::PgPool;
|
||||
use tokio::net::TcpListener;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
use zero2prod::{configuration::get_configuration, startup::run};
|
||||
use zero2prod::{configuration::get_configuration, startup::run, telemetry::init_subscriber};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
format!(
|
||||
"{}=debug,tower_http=debug,axum::rejection=trace",
|
||||
env!("CARGO_CRATE_NAME")
|
||||
)
|
||||
.into()
|
||||
}),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
init_subscriber(std::io::stdout);
|
||||
let configuration = get_configuration().expect("Failed to read configuration");
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", configuration.application_port))
|
||||
.await
|
||||
.unwrap();
|
||||
tracing::debug!("listening on {}", listener.local_addr().unwrap());
|
||||
let connection_pool = PgPool::connect(&configuration.database.connection_string())
|
||||
let connection_pool =
|
||||
PgPool::connect(configuration.database.connection_string().expose_secret())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -4,32 +4,53 @@ use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "Adding a new subscriber",
|
||||
skip(connection, form),
|
||||
fields(
|
||||
subscriber_email = %form.email,
|
||||
subscriber_name = %form.name
|
||||
)
|
||||
)]
|
||||
pub async fn subscribe(
|
||||
State(connection): State<PgPool>,
|
||||
form: Form<FormData>,
|
||||
) -> impl IntoResponse {
|
||||
match sqlx::query!(
|
||||
if insert_subscriber(&connection, &form).await.is_err() {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
} else {
|
||||
StatusCode::OK
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "Saving new subscriber details in the database",
|
||||
skip(connection, form)
|
||||
)]
|
||||
pub async fn insert_subscriber(
|
||||
connection: &PgPool,
|
||||
form: &Form<FormData>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
insert into subscriptions (id, email, name, subscribed_at)
|
||||
values ($1, $2, $3, $4);
|
||||
INSERT INTO subscriptions (id, email, name, subscribed_at)
|
||||
VALUES ($1, $2, $3, $4);
|
||||
"#,
|
||||
Uuid::new_v4(),
|
||||
form.email,
|
||||
form.name,
|
||||
Utc::now()
|
||||
)
|
||||
.execute(&connection)
|
||||
.execute(connection)
|
||||
.await
|
||||
{
|
||||
Ok(_) => StatusCode::OK,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to execute query: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to execute query: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct FormData {
|
||||
name: String,
|
||||
|
||||
@@ -8,6 +8,7 @@ use axum::{
|
||||
use sqlx::PgPool;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn run(listener: TcpListener, connection_pool: PgPool) {
|
||||
axum::serve(listener, app(connection_pool)).await.unwrap();
|
||||
@@ -23,11 +24,13 @@ pub fn app(connection_pool: PgPool) -> Router {
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
|
||||
tracing::info_span!(
|
||||
"http_request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
request_id,
|
||||
some_other_field = tracing::field::Empty,
|
||||
)
|
||||
}),
|
||||
|
||||
22
src/telemetry.rs
Normal file
22
src/telemetry.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
|
||||
use tracing_subscriber::{fmt::MakeWriter, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
pub fn init_subscriber<Sink>(sink: Sink)
|
||||
where
|
||||
Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static,
|
||||
{
|
||||
let formatting_layer = BunyanFormattingLayer::new(env!("CARGO_CRATE_NAME").into(), sink);
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
format!(
|
||||
"{}=debug,tower_http=debug,axum::rejection=trace",
|
||||
env!("CARGO_CRATE_NAME")
|
||||
)
|
||||
.into()
|
||||
}),
|
||||
)
|
||||
.with(JsonStorageLayer)
|
||||
.with(formatting_layer)
|
||||
.init();
|
||||
}
|
||||
@@ -1,7 +1,20 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use secrecy::ExposeSecret;
|
||||
use sqlx::{Connection, Executor, PgConnection, PgPool};
|
||||
use tokio::net::TcpListener;
|
||||
use uuid::Uuid;
|
||||
use zero2prod::configuration::{DatabaseSettings, get_configuration};
|
||||
use zero2prod::{
|
||||
configuration::{DatabaseSettings, get_configuration},
|
||||
telemetry::init_subscriber,
|
||||
};
|
||||
|
||||
static TRACING: Lazy<()> = Lazy::new(|| {
|
||||
if std::env::var("TEST_LOG").is_ok() {
|
||||
init_subscriber(std::io::stdout);
|
||||
} else {
|
||||
init_subscriber(std::io::sink);
|
||||
}
|
||||
});
|
||||
|
||||
pub struct TestApp {
|
||||
pub address: String,
|
||||
@@ -28,7 +41,7 @@ async fn subscribe_returns_a_200_for_valid_form_data() {
|
||||
let app = spawn_app().await;
|
||||
let configuration = get_configuration().expect("Failed to read configuration");
|
||||
let connection_string = configuration.database.connection_string();
|
||||
let mut connection = PgConnection::connect(&connection_string)
|
||||
let mut connection = PgConnection::connect(connection_string.expose_secret())
|
||||
.await
|
||||
.expect("Failed to connect to Postgres");
|
||||
let client = reqwest::Client::new();
|
||||
@@ -82,6 +95,8 @@ async fn subscribe_returns_a_422_when_data_is_missing() {
|
||||
}
|
||||
|
||||
async fn spawn_app() -> TestApp {
|
||||
Lazy::force(&TRACING);
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap().to_string();
|
||||
|
||||
@@ -102,7 +117,7 @@ async fn spawn_app() -> TestApp {
|
||||
}
|
||||
|
||||
async fn configure_database(config: &DatabaseSettings) -> PgPool {
|
||||
let connection = PgPool::connect(&config.connection_string_without_db())
|
||||
let connection = PgPool::connect(config.connection_string_without_db().expose_secret())
|
||||
.await
|
||||
.expect("Failed to connect to Postgres");
|
||||
connection
|
||||
@@ -110,7 +125,7 @@ async fn configure_database(config: &DatabaseSettings) -> PgPool {
|
||||
.await
|
||||
.expect("Failed to create the database");
|
||||
|
||||
let connection_pool = PgPool::connect(&config.connection_string())
|
||||
let connection_pool = PgPool::connect(config.connection_string().expose_secret())
|
||||
.await
|
||||
.expect("Failed to connect to Postgres");
|
||||
sqlx::migrate!("./migrations")
|
||||
|
||||
Reference in New Issue
Block a user