This commit is contained in:
2023-08-29 21:34:01 +02:00
parent 3719dfcef6
commit b80cae1278
11 changed files with 431 additions and 237 deletions

12
rust/Cargo.lock generated
View File

@@ -1225,6 +1225,7 @@ dependencies = [
"opentelemetry", "opentelemetry",
"opentelemetry-semantic-conventions", "opentelemetry-semantic-conventions",
"thrift", "thrift",
"tokio",
] ]
[[package]] [[package]]
@@ -1270,6 +1271,8 @@ dependencies = [
"rand", "rand",
"regex", "regex",
"thiserror", "thiserror",
"tokio",
"tokio-stream",
] ]
[[package]] [[package]]
@@ -1318,6 +1321,7 @@ dependencies = [
"tower", "tower",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-log",
"tracing-opentelemetry", "tracing-opentelemetry",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",
@@ -1667,9 +1671,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.101.3" version = "0.101.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d"
dependencies = [ dependencies = [
"ring", "ring",
"untrusted", "untrusted",
@@ -1808,9 +1812,9 @@ dependencies = [
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.8" version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [ dependencies = [
"autocfg", "autocfg",
] ]

View File

@@ -11,10 +11,18 @@ path = "src/main.rs"
opt-level = 0 opt-level = 0
lto = "off" lto = "off"
[dependencies] [dependencies.opentelemetry]
opentelemetry = "0.20" version = "0.20"
tracing-opentelemetry = "0.20"
opentelemetry-jaeger = "0.19" [dependencies.tracing-opentelemetry]
version = "0.20"
[dependencies.tracing-log]
version = "0.1"
[dependencies.opentelemetry-jaeger]
version = "0.19"
features = ["rt-tokio"]
[dependencies.http] [dependencies.http]
version = "0.2" version = "0.2"

View File

@@ -1,4 +1,5 @@
use axum::{extract::State, middleware::Next, response::IntoResponse}; use axum::{extract::State, middleware::Next, response::IntoResponse};
use tracing::Instrument;
use hyper::Request; use hyper::Request;
@@ -11,47 +12,61 @@ pub enum Config {
Disabled { assume_user: String }, Disabled { assume_user: String },
} }
#[tracing::instrument(skip(state, request, next))] #[tracing::instrument(name = "check_auth", skip(state, request, next))]
pub async fn authorize<B>( pub async fn authorize<B>(
State(state): State<AppState>, State(state): State<AppState>,
mut request: Request<B>, mut request: Request<B>,
next: Next<B>, next: Next<B>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let current_user = match state.auth_config { let current_user = async {
Config::Disabled { assume_user } => { let user = match state.auth_config {
match models::user::User::find_by_name(&state.database_pool, &assume_user).await? { Config::Disabled { assume_user } => {
Some(user) => user, let user =
None => { match models::user::User::find_by_name(&state.database_pool, &assume_user)
return Err(Error::Request(RequestError::AuthenticationUserNotFound { .await?
username: assume_user, {
})) Some(user) => user,
} None => {
return Err(Error::Request(RequestError::AuthenticationUserNotFound {
username: assume_user,
}))
}
};
tracing::info!(?user, "auth disabled, requested user exists");
user
} }
} Config::Enabled => {
Config::Enabled => { let Some(username) = request.headers().get("x-auth-username") else {
let Some(username) = request.headers().get("x-auth-username") else {
return Err(Error::Request(RequestError::AuthenticationHeaderMissing)); 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 let user = match models::user::User::find_by_name(&state.database_pool, &username)
.to_str() .await?
.map_err(|error| { {
Error::Request(RequestError::AuthenticationHeaderInvalid { Some(user) => user,
message: error.to_string(), None => {
}) tracing::warn!(username, "auth rejected, user not found");
})? return Err(Error::Request(RequestError::AuthenticationUserNotFound {
.to_string(); username,
}));
match models::user::User::find_by_name(&state.database_pool, &username).await? { }
Some(user) => user, };
None => { tracing::info!(?user, "auth successful");
return Err(Error::Request(RequestError::AuthenticationUserNotFound { user
username,
}))
}
} }
} };
}; Ok(user)
}
.instrument(tracing::debug_span!("authorize"))
.await?;
request.extensions_mut().insert(current_user); request.extensions_mut().insert(current_user);
Ok(next.run(request).await) Ok(next.run(request).await)

69
rust/src/cmd.rs Normal file
View File

@@ -0,0 +1,69 @@
use clap::{Parser, Subcommand, ValueEnum};
#[derive(ValueEnum, Clone, Copy, Debug)]
pub enum BoolArg {
True,
False,
}
impl From<BoolArg> 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<String>,
}
#[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,
}

View File

@@ -3,6 +3,7 @@ use uuid::Uuid;
use std::fmt; use std::fmt;
pub mod auth; pub mod auth;
pub mod cmd;
pub mod error; pub mod error;
pub mod htmx; pub mod htmx;
pub mod models; pub mod models;

View File

@@ -1,57 +1,11 @@
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::process::ExitCode; use std::process::ExitCode;
use std::str::FromStr; 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}; use clap::Parser;
#[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<String>,
}
#[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,
}
struct MainResult(Result<(), Error>); struct MainResult(Result<(), Error>);
@@ -75,96 +29,119 @@ impl From<Error> for MainResult {
#[tokio::main] #[tokio::main]
async fn main() -> MainResult { async fn main() -> MainResult {
telemetry::init_tracing(); let args = cmd::Args::parse();
let args = Args::parse(); telemetry::init_tracing(
match args.command { if args.enable_opentelemetry.into() {
Command::Serve(serve_args) => { telemetry::OpenTelemetryConfig::Enabled
if let Err(e) = sqlite::migrate(&args.database_url).await { } else {
return <_ as Into<Error>>::into(e).into(); telemetry::OpenTelemetryConfig::Disabled
}
let database_pool = match sqlite::init_database_pool(&args.database_url).await {
Ok(pool) => pool,
Err(e) => return <_ as Into<Error>>::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 <hyper::Error as Into<Error>>::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<Error>>::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
)
}
},
}, },
Command::Migrate => { if args.enable_tokio_console.into() {
if let Err(e) = sqlite::migrate(&args.database_url).await { telemetry::TokioConsoleConfig::Enabled
return <_ as Into<Error>>::into(e).into(); } else {
} telemetry::TokioConsoleConfig::Disabled
},
args,
|args| -> Pin<Box<dyn std::future::Future<Output = MainResult>>> {
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<Error>>::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<Error>>::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 <hyper::Error as Into<Error>>::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<Error>>::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<Error>>::into(e).into();
}
println!("Migrations successfully applied");
}
}
MainResult(Ok(()))
})
},
)
.await
} }

View File

@@ -4,6 +4,8 @@ use crate::Context;
use futures::{TryFutureExt, TryStreamExt}; use futures::{TryFutureExt, TryStreamExt};
use uuid::Uuid; use uuid::Uuid;
use tracing::Instrument;
pub struct Inventory { pub struct Inventory {
pub categories: Vec<Category>, pub categories: Vec<Category>,
} }
@@ -12,26 +14,32 @@ impl Inventory {
#[tracing::instrument] #[tracing::instrument]
pub async fn load(ctx: &Context, pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<Self, Error> { pub async fn load(ctx: &Context, pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<Self, Error> {
let user_id = ctx.user.id.to_string(); let user_id = ctx.user.id.to_string();
let mut categories = sqlx::query_as!( let categories = async {
DbCategoryRow, let mut categories = sqlx::query_as!(
"SELECT DbCategoryRow,
"SELECT
id, id,
name, name,
description description
FROM inventory_items_categories FROM inventory_items_categories
WHERE user_id = ?", WHERE user_id = ?",
user_id, user_id,
) )
.fetch(pool) .fetch(pool)
.map_ok(|row: DbCategoryRow| row.try_into()) .map_ok(|row: DbCategoryRow| row.try_into())
.try_collect::<Vec<Result<Category, Error>>>() .try_collect::<Vec<Result<Category, Error>>>()
.await? .await?
.into_iter() .into_iter()
.collect::<Result<Vec<Category>, Error>>()?; .collect::<Result<Vec<Category>, Error>>()?;
for category in &mut categories { for category in &mut categories {
category.populate_items(ctx, pool).await?; category.populate_items(ctx, pool).await?;
}
Ok::<_, Error>(categories)
} }
.instrument(tracing::info_span!("packager::query", "query"))
.await?;
Ok(Self { categories }) Ok(Self { categories })
} }

View File

@@ -1,10 +1,15 @@
use axum::{ use axum::{
error_handling::HandleErrorLayer,
http::header::HeaderMap, http::header::HeaderMap,
http::StatusCode,
middleware, middleware,
routing::{get, post}, routing::{get, post},
Router, BoxError, Router,
}; };
use std::time::Duration;
use tower::{timeout::TimeoutLayer, ServiceBuilder};
use crate::{AppState, Error, RequestError, TopLevelPage}; use crate::{AppState, Error, RequestError, TopLevelPage};
use super::auth; 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)) .route("/debug", get(debug))
.merge( .merge(
// thse are routes that require authentication // thse are routes that require authentication
@@ -119,6 +131,15 @@ pub fn router(state: AppState) -> Router {
auth::authorize, 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 { .fallback(|| async {
Error::Request(RequestError::NotFound { Error::Request(RequestError::NotFound {
message: "no route found".to_string(), message: "no route found".to_string(),

View File

@@ -1,5 +1,7 @@
use std::time; use std::time;
use tracing::Instrument;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use sqlx::ConnectOptions; use sqlx::ConnectOptions;
pub use sqlx::{Pool, Sqlite}; pub use sqlx::{Pool, Sqlite};
@@ -28,10 +30,13 @@ pub async fn migrate(url: &str) -> Result<(), StartError> {
.connect_with( .connect_with(
SqliteConnectOptions::from_str(url)? SqliteConnectOptions::from_str(url)?
.pragma("foreign_keys", "0") .pragma("foreign_keys", "0")
.log_statements(log::LevelFilter::Debug), .log_statements(log::LevelFilter::Warn),
) )
.await?; .await?;
sqlx::migrate!().run(&pool).await?; async { sqlx::migrate!().run(&pool).await }
.instrument(tracing::info_span!("packager::query", "migration"))
.await?;
Ok(()) Ok(())
} }

View File

@@ -1,12 +1,15 @@
use std::fmt; use std::fmt;
use std::future::Future;
use std::io; use std::io;
use std::pin::Pin;
use std::time::Duration; use std::time::Duration;
use axum::Router; use axum::Router;
use http::Request; use http::Request;
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
use tracing::{Level, Span}; use tracing::Span;
use tracing_log::LogTracer;
use tracing_subscriber::{ use tracing_subscriber::{
filter::{LevelFilter, Targets}, filter::{LevelFilter, Targets},
fmt::{format::Format, Layer}, fmt::{format::Format, Layer},
@@ -16,13 +19,31 @@ use tracing_subscriber::{
}; };
use uuid::Uuid; use uuid::Uuid;
use opentelemetry::global; use opentelemetry::{global, runtime::Tokio};
pub fn otel_init(f: impl FnOnce() -> ()) { pub enum OpenTelemetryConfig {
f() Enabled,
Disabled,
} }
pub fn init_tracing() { pub enum TokioConsoleConfig {
Enabled,
Disabled,
}
pub async fn init_tracing<Func, T>(
opentelemetry_config: OpenTelemetryConfig,
tokio_console_config: TokioConsoleConfig,
args: crate::cmd::Args,
f: Func,
) -> T
where
Func: FnOnce(crate::cmd::Args) -> Pin<Box<dyn Future<Output = T>>>,
T: std::process::Termination,
{
let mut shutdown_functions: Vec<Box<dyn FnOnce() -> Result<(), Box<dyn std::error::Error>>>> =
vec![];
// default is the Full format, there is no way to specify this, but it can be // default is the Full format, there is no way to specify this, but it can be
// overridden via builder methods // overridden via builder methods
let stdout_format = Format::default() let stdout_format = Format::default()
@@ -37,53 +58,81 @@ pub fn init_tracing() {
.with_writer(io::stdout); .with_writer(io::stdout);
let stdout_filter = Targets::new() let stdout_filter = Targets::new()
.with_default(LevelFilter::OFF) .with_default(LevelFilter::WARN)
.with_targets(vec![ .with_targets(vec![
(env!("CARGO_PKG_NAME"), Level::DEBUG), (env!("CARGO_PKG_NAME"), LevelFilter::DEBUG),
// this is for axum requests ("request", LevelFilter::DEBUG),
("request", Level::DEBUG), ("runtime", LevelFilter::OFF),
// required for tokio-console as by the docs ("sqlx", LevelFilter::TRACE),
// ("tokio", Level::TRACE),
// ("runtime", Level::TRACE),
]); ]);
let stdout_layer = stdout_layer.with_filter(stdout_filter); 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()); let opentelemetry_layer = match opentelemetry_config {
// Sets up the machinery needed to export data to Jaeger OpenTelemetryConfig::Enabled => {
// There are other OTel crates that provide pipelines for the vendors global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new());
// mentioned earlier. // Sets up the machinery needed to export data to Jaeger
let tracer = opentelemetry_jaeger::new_agent_pipeline() // There are other OTel crates that provide pipelines for the vendors
.with_service_name(env!("CARGO_PKG_NAME")) // mentioned earlier.
.install_simple() let tracer = opentelemetry_jaeger::new_agent_pipeline()
.unwrap(); .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() let opentelemetry_filter = {
.with_default(LevelFilter::OFF) Targets::new()
.with_targets(vec![ .with_default(LevelFilter::DEBUG)
(env!("CARGO_PKG_NAME"), Level::DEBUG), .with_targets(vec![
// this is for axum requests (env!("CARGO_PKG_NAME"), LevelFilter::DEBUG),
("request", Level::DEBUG), ("request", LevelFilter::DEBUG),
// required for tokio-console as by the docs ("runtime", LevelFilter::OFF),
// ("tokio", Level::TRACE), ("sqlx", LevelFilter::DEBUG),
// ("runtime", Level::TRACE), ])
]); };
let opentelemetry = tracing_opentelemetry::layer() let opentelemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
.with_tracer(tracer) // .with_filter(opentelemetry_filter);
.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() let registry = Registry::default()
.with(console_layer) .with(console_layer)
.with(opentelemetry) .with(opentelemetry_layer)
// just an example, you can actuall pass Options here for layers that might be // just an example, you can actuall pass Options here for layers that might be
// set/unset at runtime // set/unset at runtime
.with(Some(stdout_layer)) .with(Some(stdout_layer))
.with(None::<Layer<_>>); .with(None::<Layer<_>>);
tracing::subscriber::set_global_default(registry).unwrap(); 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); struct Latency(Duration);

View File

@@ -7,12 +7,17 @@ use uuid::Uuid;
pub struct Inventory; pub struct Inventory;
impl Inventory { impl Inventory {
#[tracing::instrument] #[tracing::instrument(
target = "packager::html::build",
name = "build_inventory",
fields(component = "Inventory")
)]
pub fn build( pub fn build(
active_category: Option<&models::inventory::Category>, active_category: Option<&models::inventory::Category>,
categories: &Vec<models::inventory::Category>, categories: &Vec<models::inventory::Category>,
edit_item_id: Option<Uuid>, edit_item_id: Option<Uuid>,
) -> Markup { ) -> Markup {
tracing::info!("building inventory HTML component");
html!( html!(
div id="pkglist-item-manager" { div id="pkglist-item-manager" {
div ."p-8" ."grid" ."grid-cols-4" ."gap-5" { div ."p-8" ."grid" ."grid-cols-4" ."gap-5" {
@@ -37,7 +42,11 @@ impl Inventory {
pub struct InventoryCategoryList; pub struct InventoryCategoryList;
impl InventoryCategoryList { impl InventoryCategoryList {
#[tracing::instrument] #[tracing::instrument(
target = "packager::html::build",
name = "build_inventory_category_list",
fields(component = "InventoryCategoryList")
)]
pub fn build( pub fn build(
active_category: Option<&models::inventory::Category>, active_category: Option<&models::inventory::Category>,
categories: &Vec<models::inventory::Category>, categories: &Vec<models::inventory::Category>,
@@ -144,7 +153,11 @@ impl InventoryCategoryList {
pub struct InventoryItemList; pub struct InventoryItemList;
impl 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<Uuid>, items: &Vec<models::inventory::Item>) -> Markup { pub fn build(edit_item_id: Option<Uuid>, items: &Vec<models::inventory::Item>) -> Markup {
let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1); let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1);
html!( html!(
@@ -320,7 +333,11 @@ impl InventoryItemList {
pub struct InventoryNewItemFormName; pub struct InventoryNewItemFormName;
impl 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 { pub fn build(value: Option<&str>, error: bool) -> Markup {
html!( html!(
div div
@@ -368,7 +385,11 @@ impl InventoryNewItemFormName {
pub struct InventoryNewItemFormWeight; pub struct InventoryNewItemFormWeight;
impl 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 { pub fn build() -> Markup {
html!( html!(
div div
@@ -411,7 +432,11 @@ impl InventoryNewItemFormWeight {
pub struct InventoryNewItemFormCategory; pub struct InventoryNewItemFormCategory;
impl InventoryNewItemFormCategory { impl InventoryNewItemFormCategory {
#[tracing::instrument] #[tracing::instrument(
target = "packager::html::build",
name = "build_inventory_new_item_form_category",
fields(component = "InventoryNewItemFormCategory")
)]
pub fn build( pub fn build(
active_category: Option<&models::inventory::Category>, active_category: Option<&models::inventory::Category>,
categories: &Vec<models::inventory::Category>, categories: &Vec<models::inventory::Category>,
@@ -452,7 +477,11 @@ impl InventoryNewItemFormCategory {
pub struct InventoryNewItemForm; pub struct InventoryNewItemForm;
impl InventoryNewItemForm { impl InventoryNewItemForm {
#[tracing::instrument] #[tracing::instrument(
target = "packager::html::build",
name = "build_inventory_new_item_form",
fields(component = "InventoryNewItemForm")
)]
pub fn build( pub fn build(
active_category: Option<&models::inventory::Category>, active_category: Option<&models::inventory::Category>,
categories: &Vec<models::inventory::Category>, categories: &Vec<models::inventory::Category>,
@@ -499,7 +528,11 @@ impl InventoryNewItemForm {
pub struct InventoryNewCategoryForm; pub struct InventoryNewCategoryForm;
impl InventoryNewCategoryForm { impl InventoryNewCategoryForm {
#[tracing::instrument] #[tracing::instrument(
target = "packager::html::build",
name = "build_inventory_new_category_form",
fields(component = "InventoryNewCategoryForm")
)]
pub fn build() -> Markup { pub fn build() -> Markup {
html!( html!(
form form
@@ -554,7 +587,11 @@ impl InventoryNewCategoryForm {
pub struct InventoryItem; pub struct InventoryItem;
impl 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 { pub fn build(_state: &ClientState, item: &models::inventory::InventoryItem) -> Markup {
html!( html!(
div ."p-8" { div ."p-8" {