diff --git a/rust/Cargo.lock b/rust/Cargo.lock index bd7ba3b..542bd8e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1225,6 +1225,7 @@ dependencies = [ "opentelemetry", "opentelemetry-semantic-conventions", "thrift", + "tokio", ] [[package]] @@ -1270,6 +1271,8 @@ dependencies = [ "rand", "regex", "thiserror", + "tokio", + "tokio-stream", ] [[package]] @@ -1318,6 +1321,7 @@ dependencies = [ "tower", "tower-http", "tracing", + "tracing-log", "tracing-opentelemetry", "tracing-subscriber", "uuid", @@ -1667,9 +1671,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.3" +version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ "ring", "untrusted", @@ -1808,9 +1812,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9657687..890b79e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,10 +11,18 @@ path = "src/main.rs" opt-level = 0 lto = "off" -[dependencies] -opentelemetry = "0.20" -tracing-opentelemetry = "0.20" -opentelemetry-jaeger = "0.19" +[dependencies.opentelemetry] +version = "0.20" + +[dependencies.tracing-opentelemetry] +version = "0.20" + +[dependencies.tracing-log] +version = "0.1" + +[dependencies.opentelemetry-jaeger] +version = "0.19" +features = ["rt-tokio"] [dependencies.http] version = "0.2" diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 28b0eb5..d4e8f44 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,4 +1,5 @@ use axum::{extract::State, middleware::Next, response::IntoResponse}; +use tracing::Instrument; use hyper::Request; @@ -11,47 +12,61 @@ pub enum Config { Disabled { assume_user: String }, } -#[tracing::instrument(skip(state, request, next))] +#[tracing::instrument(name = "check_auth", skip(state, request, next))] pub async fn authorize( State(state): State, mut request: Request, next: Next, ) -> Result { - let current_user = match state.auth_config { - Config::Disabled { assume_user } => { - match models::user::User::find_by_name(&state.database_pool, &assume_user).await? { - Some(user) => user, - None => { - return Err(Error::Request(RequestError::AuthenticationUserNotFound { - username: assume_user, - })) - } + let current_user = async { + let user = match state.auth_config { + Config::Disabled { assume_user } => { + let user = + match models::user::User::find_by_name(&state.database_pool, &assume_user) + .await? + { + Some(user) => user, + None => { + return Err(Error::Request(RequestError::AuthenticationUserNotFound { + username: assume_user, + })) + } + }; + tracing::info!(?user, "auth disabled, requested user exists"); + user } - } - Config::Enabled => { - let Some(username) = request.headers().get("x-auth-username") else { + Config::Enabled => { + let Some(username) = request.headers().get("x-auth-username") else { return Err(Error::Request(RequestError::AuthenticationHeaderMissing)); }; + let username = username + .to_str() + .map_err(|error| { + Error::Request(RequestError::AuthenticationHeaderInvalid { + message: error.to_string(), + }) + })? + .to_string(); - let username = username - .to_str() - .map_err(|error| { - Error::Request(RequestError::AuthenticationHeaderInvalid { - message: error.to_string(), - }) - })? - .to_string(); - - match models::user::User::find_by_name(&state.database_pool, &username).await? { - Some(user) => user, - None => { - return Err(Error::Request(RequestError::AuthenticationUserNotFound { - username, - })) - } + let user = match models::user::User::find_by_name(&state.database_pool, &username) + .await? + { + Some(user) => user, + None => { + tracing::warn!(username, "auth rejected, user not found"); + return Err(Error::Request(RequestError::AuthenticationUserNotFound { + username, + })); + } + }; + tracing::info!(?user, "auth successful"); + user } - } - }; + }; + Ok(user) + } + .instrument(tracing::debug_span!("authorize")) + .await?; request.extensions_mut().insert(current_user); Ok(next.run(request).await) diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs new file mode 100644 index 0000000..e40298d --- /dev/null +++ b/rust/src/cmd.rs @@ -0,0 +1,69 @@ +use clap::{Parser, Subcommand, ValueEnum}; + +#[derive(ValueEnum, Clone, Copy, Debug)] +pub enum BoolArg { + True, + False, +} + +impl From for bool { + fn from(arg: BoolArg) -> bool { + match arg { + BoolArg::True => true, + BoolArg::False => false, + } + } +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + #[arg(long)] + pub database_url: String, + + #[arg(long, value_enum, default_value_t = BoolArg::False)] + pub enable_opentelemetry: BoolArg, + + #[arg(long, value_enum, default_value_t = BoolArg::False)] + pub enable_tokio_console: BoolArg, + + #[command(subcommand)] + pub command: Command, +} + +#[derive(Subcommand, Debug)] +pub enum Command { + Serve(Serve), + #[command(subcommand)] + Admin(Admin), + Migrate, +} + +#[derive(Parser, Debug)] +pub struct Serve { + #[arg(long, default_value_t = 3000)] + pub port: u16, + #[arg(long)] + pub bind: String, + #[arg(long, name = "USERNAME")] + pub disable_auth_and_assume_user: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Admin { + #[command(subcommand)] + User(UserCommand), +} + +#[derive(Subcommand, Debug)] +pub enum UserCommand { + Create(UserCreate), +} + +#[derive(Parser, Debug)] +pub struct UserCreate { + #[arg(long)] + pub username: String, + #[arg(long)] + pub fullname: String, +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3f86946..9fbae7d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,6 +3,7 @@ use uuid::Uuid; use std::fmt; pub mod auth; +pub mod cmd; pub mod error; pub mod htmx; pub mod models; diff --git a/rust/src/main.rs b/rust/src/main.rs index b161e75..9888820 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,57 +1,11 @@ use std::net::{IpAddr, SocketAddr}; +use std::pin::Pin; use std::process::ExitCode; use std::str::FromStr; -use clap::{Parser, Subcommand}; +use packager::{auth, cmd, models, routing, sqlite, telemetry, AppState, ClientState, Error}; -use packager::{auth, models, routing, sqlite, telemetry, AppState, ClientState, Error}; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - #[arg(long)] - database_url: String, - - #[command(subcommand)] - command: Command, -} - -#[derive(Subcommand, Debug)] -enum Command { - Serve(Serve), - #[command(subcommand)] - Admin(Admin), - Migrate, -} - -#[derive(Parser, Debug)] -struct Serve { - #[arg(long, default_value_t = 3000)] - port: u16, - #[arg(long)] - bind: String, - #[arg(long, name = "USERNAME")] - disable_auth_and_assume_user: Option, -} - -#[derive(Subcommand, Debug)] -enum Admin { - #[command(subcommand)] - User(UserCommand), -} - -#[derive(Subcommand, Debug)] -enum UserCommand { - Create(UserCreate), -} - -#[derive(Parser, Debug)] -struct UserCreate { - #[arg(long)] - username: String, - #[arg(long)] - fullname: String, -} +use clap::Parser; struct MainResult(Result<(), Error>); @@ -75,96 +29,119 @@ impl From for MainResult { #[tokio::main] async fn main() -> MainResult { - telemetry::init_tracing(); - let args = Args::parse(); - match args.command { - Command::Serve(serve_args) => { - if let Err(e) = sqlite::migrate(&args.database_url).await { - return <_ as Into>::into(e).into(); - } - - let database_pool = match sqlite::init_database_pool(&args.database_url).await { - Ok(pool) => pool, - Err(e) => return <_ as Into>::into(e).into(), - }; - - let state = AppState { - database_pool, - client_state: ClientState::new(), - auth_config: if let Some(assume_user) = serve_args.disable_auth_and_assume_user { - auth::Config::Disabled { assume_user } - } else { - auth::Config::Enabled - }, - }; - - // build our application with a route - let app = routing::router(state); - let app = telemetry::init_request_tracing(app); - - let addr = SocketAddr::from(( - IpAddr::from_str(&serve_args.bind) - .map_err(|error| { - format!("error parsing bind address {}: {}", &serve_args.bind, error) - }) - .unwrap(), - serve_args.port, - )); - tracing::debug!("listening on {}", addr); - if let Err(e) = axum::Server::try_bind(&addr) - .map_err(|error| format!("error binding to {}: {}", addr, error)) - .unwrap() - .serve(app.into_make_service()) - .await - { - return >::into(e).into(); - } - } - Command::Admin(admin_command) => match admin_command { - Admin::User(cmd) => match cmd { - UserCommand::Create(user) => { - let database_pool = match sqlite::init_database_pool(&args.database_url).await { - Ok(pool) => pool, - Err(e) => return <_ as Into>::into(e).into(), - }; - - let id = match models::user::create( - &database_pool, - models::user::NewUser { - username: &user.username, - fullname: &user.fullname, - }, - ) - .await - .map_err(|error| match error { - models::Error::Query(models::QueryError::Duplicate { description: _ }) => { - Error::Command(packager::CommandError::UserExists { - username: user.username.clone(), - }) - } - _ => Error::Model(error), - }) { - Ok(id) => id, - Err(e) => { - return e.into(); - } - }; - - println!( - "User \"{}\" created successfully (id {})", - &user.username, id - ) - } - }, + let args = cmd::Args::parse(); + telemetry::init_tracing( + if args.enable_opentelemetry.into() { + telemetry::OpenTelemetryConfig::Enabled + } else { + telemetry::OpenTelemetryConfig::Disabled }, - Command::Migrate => { - if let Err(e) = sqlite::migrate(&args.database_url).await { - return <_ as Into>::into(e).into(); - } + if args.enable_tokio_console.into() { + telemetry::TokioConsoleConfig::Enabled + } else { + telemetry::TokioConsoleConfig::Disabled + }, + args, + |args| -> Pin>> { + Box::pin(async move { + match args.command { + cmd::Command::Serve(serve_args) => { + if let Err(e) = sqlite::migrate(&args.database_url).await { + return <_ as Into>::into(e).into(); + } - println!("Migrations successfully applied"); - } - } + let database_pool = + match sqlite::init_database_pool(&args.database_url).await { + Ok(pool) => pool, + Err(e) => return <_ as Into>::into(e).into(), + }; - MainResult(Ok(())) + let state = AppState { + database_pool, + client_state: ClientState::new(), + auth_config: if let Some(assume_user) = + serve_args.disable_auth_and_assume_user + { + auth::Config::Disabled { assume_user } + } else { + auth::Config::Enabled + }, + }; + + // build our application with a route + let app = routing::router(state); + let app = telemetry::init_request_tracing(app); + + let addr = SocketAddr::from(( + IpAddr::from_str(&serve_args.bind) + .map_err(|error| { + format!( + "error parsing bind address {}: {}", + &serve_args.bind, error + ) + }) + .unwrap(), + serve_args.port, + )); + tracing::debug!("listening on {}", addr); + if let Err(e) = axum::Server::try_bind(&addr) + .map_err(|error| format!("error binding to {}: {}", addr, error)) + .unwrap() + .serve(app.into_make_service()) + .await + { + return >::into(e).into(); + } + } + cmd::Command::Admin(admin_command) => match admin_command { + cmd::Admin::User(cmd) => match cmd { + cmd::UserCommand::Create(user) => { + let database_pool = + match sqlite::init_database_pool(&args.database_url).await { + Ok(pool) => pool, + Err(e) => return <_ as Into>::into(e).into(), + }; + + let id = match models::user::create( + &database_pool, + models::user::NewUser { + username: &user.username, + fullname: &user.fullname, + }, + ) + .await + .map_err(|error| match error { + models::Error::Query(models::QueryError::Duplicate { + description: _, + }) => Error::Command(packager::CommandError::UserExists { + username: user.username.clone(), + }), + _ => Error::Model(error), + }) { + Ok(id) => id, + Err(e) => { + return e.into(); + } + }; + + println!( + "User \"{}\" created successfully (id {})", + &user.username, id + ) + } + }, + }, + cmd::Command::Migrate => { + if let Err(e) = sqlite::migrate(&args.database_url).await { + return <_ as Into>::into(e).into(); + } + + println!("Migrations successfully applied"); + } + } + MainResult(Ok(())) + }) + }, + ) + .await } diff --git a/rust/src/models/inventory.rs b/rust/src/models/inventory.rs index f818a7d..38fc4aa 100644 --- a/rust/src/models/inventory.rs +++ b/rust/src/models/inventory.rs @@ -4,6 +4,8 @@ use crate::Context; use futures::{TryFutureExt, TryStreamExt}; use uuid::Uuid; +use tracing::Instrument; + pub struct Inventory { pub categories: Vec, } @@ -12,26 +14,32 @@ impl Inventory { #[tracing::instrument] pub async fn load(ctx: &Context, pool: &sqlx::Pool) -> Result { let user_id = ctx.user.id.to_string(); - let mut categories = sqlx::query_as!( - DbCategoryRow, - "SELECT + let categories = async { + let mut categories = sqlx::query_as!( + DbCategoryRow, + "SELECT id, name, description FROM inventory_items_categories WHERE user_id = ?", - user_id, - ) - .fetch(pool) - .map_ok(|row: DbCategoryRow| row.try_into()) - .try_collect::>>() - .await? - .into_iter() - .collect::, Error>>()?; + user_id, + ) + .fetch(pool) + .map_ok(|row: DbCategoryRow| row.try_into()) + .try_collect::>>() + .await? + .into_iter() + .collect::, Error>>()?; - for category in &mut categories { - category.populate_items(ctx, pool).await?; + for category in &mut categories { + category.populate_items(ctx, pool).await?; + } + + Ok::<_, Error>(categories) } + .instrument(tracing::info_span!("packager::query", "query")) + .await?; Ok(Self { categories }) } diff --git a/rust/src/routing/mod.rs b/rust/src/routing/mod.rs index 28ab37e..ed9ac23 100644 --- a/rust/src/routing/mod.rs +++ b/rust/src/routing/mod.rs @@ -1,10 +1,15 @@ use axum::{ + error_handling::HandleErrorLayer, http::header::HeaderMap, + http::StatusCode, middleware, routing::{get, post}, - Router, + BoxError, Router, }; +use std::time::Duration; +use tower::{timeout::TimeoutLayer, ServiceBuilder}; + use crate::{AppState, Error, RequestError, TopLevelPage}; use super::auth; @@ -39,6 +44,13 @@ pub fn router(state: AppState) -> Router { }) }), ) + .route( + "/slow", + get(|| async { + tokio::time::sleep(Duration::from_secs(1)).await; + "Ok" + }), + ) .route("/debug", get(debug)) .merge( // thse are routes that require authentication @@ -119,6 +131,15 @@ pub fn router(state: AppState) -> Router { auth::authorize, )), ) + .layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + tracing::warn!("request timeout"); + StatusCode::REQUEST_TIMEOUT + })) + .layer(TimeoutLayer::new(Duration::from_millis(500))), + ) + // .propagate_x_request_id() .fallback(|| async { Error::Request(RequestError::NotFound { message: "no route found".to_string(), diff --git a/rust/src/sqlite.rs b/rust/src/sqlite.rs index f05cd56..beee053 100644 --- a/rust/src/sqlite.rs +++ b/rust/src/sqlite.rs @@ -1,5 +1,7 @@ use std::time; +use tracing::Instrument; + use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::ConnectOptions; pub use sqlx::{Pool, Sqlite}; @@ -28,10 +30,13 @@ pub async fn migrate(url: &str) -> Result<(), StartError> { .connect_with( SqliteConnectOptions::from_str(url)? .pragma("foreign_keys", "0") - .log_statements(log::LevelFilter::Debug), + .log_statements(log::LevelFilter::Warn), ) .await?; - sqlx::migrate!().run(&pool).await?; + async { sqlx::migrate!().run(&pool).await } + .instrument(tracing::info_span!("packager::query", "migration")) + .await?; + Ok(()) } diff --git a/rust/src/telemetry.rs b/rust/src/telemetry.rs index 588693d..3f60647 100644 --- a/rust/src/telemetry.rs +++ b/rust/src/telemetry.rs @@ -1,12 +1,15 @@ use std::fmt; +use std::future::Future; use std::io; +use std::pin::Pin; use std::time::Duration; use axum::Router; use http::Request; use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; -use tracing::{Level, Span}; +use tracing::Span; +use tracing_log::LogTracer; use tracing_subscriber::{ filter::{LevelFilter, Targets}, fmt::{format::Format, Layer}, @@ -16,13 +19,31 @@ use tracing_subscriber::{ }; use uuid::Uuid; -use opentelemetry::global; +use opentelemetry::{global, runtime::Tokio}; -pub fn otel_init(f: impl FnOnce() -> ()) { - f() +pub enum OpenTelemetryConfig { + Enabled, + Disabled, } -pub fn init_tracing() { +pub enum TokioConsoleConfig { + Enabled, + Disabled, +} + +pub async fn init_tracing( + opentelemetry_config: OpenTelemetryConfig, + tokio_console_config: TokioConsoleConfig, + args: crate::cmd::Args, + f: Func, +) -> T +where + Func: FnOnce(crate::cmd::Args) -> Pin>>, + T: std::process::Termination, +{ + let mut shutdown_functions: Vec Result<(), Box>>> = + vec![]; + // default is the Full format, there is no way to specify this, but it can be // overridden via builder methods let stdout_format = Format::default() @@ -37,53 +58,81 @@ pub fn init_tracing() { .with_writer(io::stdout); let stdout_filter = Targets::new() - .with_default(LevelFilter::OFF) + .with_default(LevelFilter::WARN) .with_targets(vec![ - (env!("CARGO_PKG_NAME"), Level::DEBUG), - // this is for axum requests - ("request", Level::DEBUG), - // required for tokio-console as by the docs - // ("tokio", Level::TRACE), - // ("runtime", Level::TRACE), + (env!("CARGO_PKG_NAME"), LevelFilter::DEBUG), + ("request", LevelFilter::DEBUG), + ("runtime", LevelFilter::OFF), + ("sqlx", LevelFilter::TRACE), ]); let stdout_layer = stdout_layer.with_filter(stdout_filter); - let console_layer = console_subscriber::Builder::default().spawn(); + let console_layer = match tokio_console_config { + TokioConsoleConfig::Enabled => Some(console_subscriber::Builder::default().spawn()), + TokioConsoleConfig::Disabled => None, + }; - global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new()); - // Sets up the machinery needed to export data to Jaeger - // There are other OTel crates that provide pipelines for the vendors - // mentioned earlier. - let tracer = opentelemetry_jaeger::new_agent_pipeline() - .with_service_name(env!("CARGO_PKG_NAME")) - .install_simple() - .unwrap(); + let opentelemetry_layer = match opentelemetry_config { + OpenTelemetryConfig::Enabled => { + global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new()); + // Sets up the machinery needed to export data to Jaeger + // There are other OTel crates that provide pipelines for the vendors + // mentioned earlier. + let tracer = opentelemetry_jaeger::new_agent_pipeline() + .with_service_name(env!("CARGO_PKG_NAME")) + .with_max_packet_size(20_000) + .with_auto_split_batch(true) + .install_batch(Tokio) + .unwrap(); - let opentelemetry_filter = Targets::new() - .with_default(LevelFilter::OFF) - .with_targets(vec![ - (env!("CARGO_PKG_NAME"), Level::DEBUG), - // this is for axum requests - ("request", Level::DEBUG), - // required for tokio-console as by the docs - // ("tokio", Level::TRACE), - // ("runtime", Level::TRACE), - ]); + let opentelemetry_filter = { + Targets::new() + .with_default(LevelFilter::DEBUG) + .with_targets(vec![ + (env!("CARGO_PKG_NAME"), LevelFilter::DEBUG), + ("request", LevelFilter::DEBUG), + ("runtime", LevelFilter::OFF), + ("sqlx", LevelFilter::DEBUG), + ]) + }; - let opentelemetry = tracing_opentelemetry::layer() - .with_tracer(tracer) - .with_filter(opentelemetry_filter); + let opentelemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer); + // .with_filter(opentelemetry_filter); + + shutdown_functions.push(Box::new(|| { + println!("shutting down otel"); + global::shutdown_tracer_provider(); + Ok(()) + })); + + println!("set up otel"); + + Some(opentelemetry_layer) + } + OpenTelemetryConfig::Disabled => None, + }; let registry = Registry::default() .with(console_layer) - .with(opentelemetry) + .with(opentelemetry_layer) // just an example, you can actuall pass Options here for layers that might be // set/unset at runtime .with(Some(stdout_layer)) .with(None::>); tracing::subscriber::set_global_default(registry).unwrap(); + + tracing::debug!("tracing setup finished"); + + tracing_log::log_tracer::Builder::new().init().unwrap(); + + let result = f(args).await; + + for shutdown_func in shutdown_functions { + shutdown_func().unwrap(); + } + result } struct Latency(Duration); diff --git a/rust/src/view/inventory.rs b/rust/src/view/inventory.rs index 088f778..d2c2996 100644 --- a/rust/src/view/inventory.rs +++ b/rust/src/view/inventory.rs @@ -7,12 +7,17 @@ use uuid::Uuid; pub struct Inventory; impl Inventory { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory", + fields(component = "Inventory") + )] pub fn build( active_category: Option<&models::inventory::Category>, categories: &Vec, edit_item_id: Option, ) -> Markup { + tracing::info!("building inventory HTML component"); html!( div id="pkglist-item-manager" { div ."p-8" ."grid" ."grid-cols-4" ."gap-5" { @@ -37,7 +42,11 @@ impl Inventory { pub struct InventoryCategoryList; impl InventoryCategoryList { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory_category_list", + fields(component = "InventoryCategoryList") + )] pub fn build( active_category: Option<&models::inventory::Category>, categories: &Vec, @@ -144,7 +153,11 @@ impl InventoryCategoryList { pub struct InventoryItemList; impl InventoryItemList { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory_item_list", + fields(component = "InventoryItemList") + )] pub fn build(edit_item_id: Option, items: &Vec) -> Markup { let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1); html!( @@ -320,7 +333,11 @@ impl InventoryItemList { pub struct InventoryNewItemFormName; impl InventoryNewItemFormName { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory_new_item_form_name", + fields(component = "InventoryNewItemFormName") + )] pub fn build(value: Option<&str>, error: bool) -> Markup { html!( div @@ -368,7 +385,11 @@ impl InventoryNewItemFormName { pub struct InventoryNewItemFormWeight; impl InventoryNewItemFormWeight { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory_new_item_form_weight", + fields(component = "InventoryNewItemFormWeight") + )] pub fn build() -> Markup { html!( div @@ -411,7 +432,11 @@ impl InventoryNewItemFormWeight { pub struct InventoryNewItemFormCategory; impl InventoryNewItemFormCategory { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory_new_item_form_category", + fields(component = "InventoryNewItemFormCategory") + )] pub fn build( active_category: Option<&models::inventory::Category>, categories: &Vec, @@ -452,7 +477,11 @@ impl InventoryNewItemFormCategory { pub struct InventoryNewItemForm; impl InventoryNewItemForm { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory_new_item_form", + fields(component = "InventoryNewItemForm") + )] pub fn build( active_category: Option<&models::inventory::Category>, categories: &Vec, @@ -499,7 +528,11 @@ impl InventoryNewItemForm { pub struct InventoryNewCategoryForm; impl InventoryNewCategoryForm { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory_new_category_form", + fields(component = "InventoryNewCategoryForm") + )] pub fn build() -> Markup { html!( form @@ -554,7 +587,11 @@ impl InventoryNewCategoryForm { pub struct InventoryItem; impl InventoryItem { - #[tracing::instrument] + #[tracing::instrument( + target = "packager::html::build", + name = "build_inventory_item", + fields(component = "InventoryItem") + )] pub fn build(_state: &ClientState, item: &models::inventory::InventoryItem) -> Markup { html!( div ."p-8" {