From a149cad25fb6336ace70c5c36e90fe85590786ee Mon Sep 17 00:00:00 2001 From: Stanislav Kosorin Date: Sun, 9 Jul 2023 11:48:46 +0200 Subject: [PATCH 1/3] logger for service, simplify justfile commands --- justfile | 6 +++--- src/main.rs | 1 + templates/dependencies.toml | 4 +++- templates/src/handler.go | 18 +++++++++--------- templates/src/logger/mod.go | 34 ++++++++++++++++++++++++++++++++++ templates/src/main.go | 34 ++++++++++++++++++++++------------ templates/src/tracing/mod.go | 4 +--- templates/src/utils/common.go | 5 +++-- templates/src/utils/streams.go | 6 +++--- 9 files changed, 79 insertions(+), 33 deletions(-) create mode 100644 templates/src/logger/mod.go diff --git a/justfile b/justfile index 6dadc9b..25e60d8 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ install: cargo build --release # Run the generator with the provided specification file, and optional title and output directory -run-generator specfile_path="example/specs/basic.yaml" output="output" title="": +run specfile_path="example/specs/basic.yaml" output="output" title="": set -e if [ -z "{{title}}" ]; then \ cargo run --release -- --specification {{specfile_path}} --output {{output}}; \ @@ -21,7 +21,7 @@ test: cargo test # Generate documentation for the generator project -generate-docs: +doc: cargo doc --open # Clean up build artifacts @@ -41,7 +41,7 @@ start-service service_name: # Generate documentation for the generated microservice # Uses the path to the last generated output directory with the provided service_name -generate-service-docs service_name: +service-doc service_name: if [ -f .last_output_directory ]; then \ output_directory=$(cat .last_output_directory); \ cd "$output_directory/{{service_name}}" && cargo doc --open; \ diff --git a/src/main.rs b/src/main.rs index c491185..42de490 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,7 @@ fn main() { "src/utils/common.go", "src/config/mod.go", "src/tracing/mod.go", + "src/logger/mod.go", ] .into_iter(), ); diff --git a/templates/dependencies.toml b/templates/dependencies.toml index cd04daa..80e5d46 100644 --- a/templates/dependencies.toml +++ b/templates/dependencies.toml @@ -6,4 +6,6 @@ tokio = { version = "1.28.2", features = ["full"] } dotenv = "0.15.0" clap = {version = "4.3.0", features = ["derive"]} opentelemetry = { version = "*", features = ["rt-tokio"] } -opentelemetry-jaeger = { version = "*", features = ["rt-tokio", "isahc_collector_client"] } \ No newline at end of file +opentelemetry-jaeger = { version = "*", features = ["rt-tokio", "isahc_collector_client"] } +log = "0.4.0" +env_logger = "0.10.0" \ No newline at end of file diff --git a/templates/src/handler.go b/templates/src/handler.go index 90d7b49..95e1ad1 100644 --- a/templates/src/handler.go +++ b/templates/src/handler.go @@ -1,9 +1,9 @@ use async_nats::{Client, Message, jetstream}; use async_nats::jetstream::Context; use crate::{publish_message,stream_publish_message,model::*,config::*}; -use std::time; use opentelemetry::global; use opentelemetry::trace::Tracer; +use log::{debug, warn}; {{ range .publish_channels }} @@ -25,7 +25,7 @@ use opentelemetry::trace::Tracer; {{ if .payload}} match serde_json::from_slice::<{{ .payload.struct_reference }}>(&message.message.payload.as_ref()) { Ok(deserialized_message) => { - println!("Received message {:#?}", deserialized_message); + debug!("Received message {:#?}", deserialized_message); // TODO: Replace this with your own handler code {{ if eq .payload.model_type "enum"}} match deserialized_message { @@ -33,14 +33,14 @@ use opentelemetry::trace::Tracer; {{ range .payload.related_models }} {{ $enumName }}::{{ .unique_id }}(payload) => { // TODO: Replace this with your own handler code - println!("Received message payload {{ .unique_id }} {:?}", payload); + debug!("Received message payload {{ .unique_id }} {:?}", payload); } {{ end }} } {{ end }} }, Err(_) => { - println!("Failed to deserialize message payload: {{ .unique_id }}\nOriginal message: {:#?}", message); + debug!("Failed to deserialize message payload: {{ .unique_id }}\nOriginal message: {:#?}", message); // TODO: Handle the failed deserialization here }, } @@ -61,17 +61,17 @@ use opentelemetry::trace::Tracer; {{ range .payload.related_models }} {{ $enumName }}::{{ .unique_id }}(payload) => { // TODO: Replace this with your own handler code - println!("Received message payload {{ .unique_id }} {:?}", payload); + debug!("Received message payload {{ .unique_id }} {:?}", payload); } {{ end }} } {{else}} - println!("Received message {:#?}", deserialized_message); + debug!("Received message {:#?}", deserialized_message); // TODO: Replace this with your own handler code {{ end }} }, Err(_) => { - println!("Failed to deserialize message payload: {{ .unique_id }}\nOriginal message: {:#?}", message); + warn!("Failed to deserialize message payload: {{ .unique_id }}\nOriginal message: {:#?}", message); // TODO: Handle the failed deserialization here }, } @@ -105,7 +105,7 @@ use opentelemetry::trace::Tracer; let payload = match serde_json::to_string(&payload) { Ok(payload) => payload, Err(_) => { - println!("Failed to serialize message payload: {{ .payload.struct_reference }}"); + warn!("Failed to serialize message payload: {{ .payload.struct_reference }}"); return; } }; @@ -125,7 +125,7 @@ use opentelemetry::trace::Tracer; let payload = match serde_json::to_string(&payload) { Ok(payload) => payload, Err(_) => { - println!("Failed to serialize message payload: {{ .payload.struct_reference }}"); + warn!("Failed to serialize message payload: {{ .payload.struct_reference }}"); return; } }; diff --git a/templates/src/logger/mod.go b/templates/src/logger/mod.go new file mode 100644 index 0000000..ef9f26a --- /dev/null +++ b/templates/src/logger/mod.go @@ -0,0 +1,34 @@ +use std::str::FromStr; +use log::LevelFilter; + +#[derive(Debug)] +pub enum LogLevelParseError { + InvalidLogLevel, +} + +pub struct LogLevel(LevelFilter); + +impl FromStr for LogLevel { + type Err = LogLevelParseError; + + fn from_str(s: &str) -> Result { + let level = match s { + "OFF" => Ok(LevelFilter::Off), + "ERROR" => Ok(LevelFilter::Error), + "WARN" => Ok(LevelFilter::Warn), + "INFO" => Ok(LevelFilter::Info), + "DEBUG" => Ok(LevelFilter::Debug), + "TRACE" => Ok(LevelFilter::Trace), + _ => Err(LogLevelParseError::InvalidLogLevel), + }?; + Ok(LogLevel(level)) + } +} + +pub fn init_logger(log_level: &str) { + let log_level: LevelFilter = match log_level.parse::() { + Ok(LogLevel(level)) => level, + Err(_) => panic!("Invalid log level"), + }; + env_logger::Builder::new().filter(None, log_level).init(); +} \ No newline at end of file diff --git a/templates/src/main.go b/templates/src/main.go index 44d1cb2..35c0775 100644 --- a/templates/src/main.go +++ b/templates/src/main.go @@ -8,24 +8,32 @@ use utils::*; use crate::handler::*; use async_nats::jetstream::{self}; use std::{collections::HashMap}; -use dotenv::dotenv; +use log::info; mod config; mod tracing; - +mod logger; #[tokio::main] async fn main() -> Result<(), async_nats::Error> { + // Load .env file let env: HashMap = config::get_env(); - let args = cli::Args::parse(); + // Initialize logger + let log_lvl = env.get("LOG_LEVEL").unwrap().parse().unwrap_or("INFO".to_string()); + logger::init_logger(&log_lvl); + + // Initialize tracing let tracing_enabled: bool = env.get("TRACING_ENABLED").unwrap().parse().unwrap(); - if (tracing_enabled) { - // Initialize Jaeger Tracer - let tracer = tracing::init_jaeger_tracer("{{ .title}}"); + if tracing_enabled { + let _tracer = tracing::init_jaeger_tracer("{{ .title}}"); } + + // Connect to NATS server + let nats_url = env.get("SERVER_URL").unwrap(); + info!("Connecting to a NATS server: {}", nats_url); + let client = async_nats::connect(nats_url).await?; - let client = async_nats::connect(env.get("SERVER_URL").unwrap()).await?; - + // Subscribe to channels {{ range .publish_channels }} {{ if (index . 1).original_operation.bindings }} {{ if (index . 1).original_operation.bindings.nats.queue }} @@ -42,9 +50,11 @@ async fn main() -> Result<(), async_nats::Error> { {{end}} {{end}} + // Parse CLI arguments + let args = cli::Args::parse(); handle_cli(&client, &args.command, &args.message).await?; - + // Listen for messages tokio::join!( {{ range .subscribe_channels }} {{ $isStream := false }} @@ -65,10 +75,10 @@ async fn main() -> Result<(), async_nats::Error> { {{ end }} ); - if (tracing_enabled) { - // Shutdown Jaeger Tracer + // Shutdown Jaeger Tracer + if tracing_enabled { tracing::shutdown_tracer_provider(); } - println!("fin"); + info!("Shutting down..."); Ok(()) } diff --git a/templates/src/tracing/mod.go b/templates/src/tracing/mod.go index 72cea56..430ad3a 100644 --- a/templates/src/tracing/mod.go +++ b/templates/src/tracing/mod.go @@ -1,10 +1,8 @@ use opentelemetry::global; -use opentelemetry::trace::Tracer; - pub fn init_jaeger_tracer(service_name: &str) { global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new()); - let tracer = opentelemetry_jaeger::new_agent_pipeline() + opentelemetry_jaeger::new_agent_pipeline() .with_service_name(service_name) .install_batch(opentelemetry::runtime::Tokio) .expect("Failed to initialize Jaeger Tracer"); diff --git a/templates/src/utils/common.go b/templates/src/utils/common.go index 7b93693..74471c9 100644 --- a/templates/src/utils/common.go +++ b/templates/src/utils/common.go @@ -1,5 +1,6 @@ use async_nats::{Client, Message, Subscriber}; use futures::StreamExt; +use log::debug; pub async fn listen_for_message<'a, F, Fut>(sub: &mut Subscriber, handler: F, client: &'a Client) where @@ -8,7 +9,7 @@ where { while let Some(message) = sub.next().await { handler(message, client).await; - println!("Message received by Subscriber: {:?}", sub); + debug!("Message received by Subscriber: {:?}", sub); } } @@ -18,6 +19,6 @@ pub async fn publish_message(client: &Client, channel: &str, payload: &str) { .publish(channel.to_string(), owned_payload) .await .unwrap(); - println!("sent"); + debug!("Published message to channel: {}", channel); } diff --git a/templates/src/utils/streams.go b/templates/src/utils/streams.go index c6a4ad2..acab230 100644 --- a/templates/src/utils/streams.go +++ b/templates/src/utils/streams.go @@ -3,6 +3,7 @@ use async_nats::Client; use async_nats::jetstream::consumer::{pull::{Config}, Consumer}; use std::time::Duration; use futures::StreamExt; +use log::debug; pub async fn stream_publish_message(client: &Context, channel: &str, payload: &str) { let owned_payload = payload.to_owned().into(); // Convert to Bytes @@ -10,7 +11,7 @@ pub async fn stream_publish_message(client: &Context, channel: &str, payload: &s .publish(channel.to_string(), owned_payload) .await .unwrap(); - println!("sent"); + debug!("Message published to channel: {}", channel); } @@ -25,13 +26,12 @@ pub async fn stream_listen_for_message( while let Some(message) = messages.next().await { let message = message?; handler(message, client); - println!( + debug!( "Message received by Subscriber: {:?}", sub.cached_info().name ); // if you show sub its a mess, is now a Context } } - Ok(()) } From e3c3274b01a063e3d3fcadb8a643b528b2fa6adf Mon Sep 17 00:00:00 2001 From: Stanislav Kosorin Date: Sun, 9 Jul 2023 12:44:03 +0200 Subject: [PATCH 2/3] update readme to reflect justfile changes, fix cargo fix --- README.md | 17 +++++++++-------- src/main.rs | 13 ++++++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8ec4941..d5b2cd8 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Crustagen is a Rust code generator that takes AsyncAPI specifications as input a - [Requirements](#requirements) - [Installation](#installation) - [Usage](#usage) + - [Limitations](#limitations) - [Contribute](#contribute) - [License](#license) @@ -25,7 +26,7 @@ Clone and build the Crustagen project: ```sh git clone https://github.com/Programmierpraktikum-MVA/AsyncAPI.git -cd crustagen +cd AsyncAPI just install # Alternatively, you can use 'cargo build --release' ``` @@ -34,7 +35,7 @@ just install # Alternatively, you can use 'cargo build --release' To generate Rust code from an AsyncAPI specification, use the following `just` command: ```sh -just run-generator specfile_path="./example/specs/basic.yaml" output="./output" # Alternatively, you can use 'cargo run -- -s ./example/specs/basic.yaml -o ./output' +just run example/specs/basic.yaml output # Alternatively, you can use 'cargo run -- -s ./example/specs/basic.yaml -o ./output' ``` This will generate a Rust project in the specified output directory. @@ -42,22 +43,22 @@ This will generate a Rust project in the specified output directory. To run the server, navigate to the output directory (replace `{project-id}` with the actual project directory name, the title of the spec) and use the `just` command: ```sh -just start-service service_name={project-id} # Alternatively, you can use 'cd output/{project-id} && cargo run' +just start-service {project-id} # Alternatively, you can use 'cd output/{project-id} && cargo run' ``` To view the auto-generated documentation, use the following command: ```sh -just generate-service-docs service_name={project-id} # Alternatively, you can use 'cd output/{project-id} && cargo doc --open' +just service-doc {project-id} # Alternatively, you can use 'cd output/{project-id} && cargo doc --open' ``` -Remember to replace `{project-id}` with the actual project directory name. +Remember to replace `{project-id}` with the name of your generated microservice (`title` field from the provided spec). ## Limitations -- only json payloads are currently supported for automatic deserialization -- only one server is currently supported and only nats protocol is supported -- only one message is currently supported per channel, payloads can be choosen freely including anyOf/oneOf/allOf +- Only json payloads are currently supported for automatic deserialization +- Only one server is currently supported and only nats protocol is supported +- Only one message is currently supported per channel, payloads can be choosen freely including anyOf/oneOf/allOf ## Contribute diff --git a/src/main.rs b/src/main.rs index 42de490..938714d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,16 +80,23 @@ fn main() { // make output a compilable project in output_path cargo_command!("init", "--bin", output_path); - // runs cargo format on path - cargo_command!("fmt", "--", output_path.join("src/main.rs")); // add dependencies append_file_to_file( template_path.join("dependencies.toml"), output_path.join("Cargo.toml"), ) .unwrap(); + + println!("āœØ Successfully added dependencies, formatting code..."); + // runs cargo format on path + cargo_command!("fmt", "--", output_path.join("src/main.rs")); // cargo fix, mostly for cleaning unused imports - cargo_command!("fix", "--bin", output_path, "--allow-dirty"); + cargo_command!( + "fix", + "--manifest-path", + output_path.join("Cargo.toml"), + "--allow-dirty" + ); if args.doc { println!("šŸ“š Generating docs..."); From 2a1afbbf71a6b66c55451115649132cb66522c89 Mon Sep 17 00:00:00 2001 From: Stanislav Kosorin Date: Sun, 9 Jul 2023 13:24:07 +0200 Subject: [PATCH 3/3] improve user experience --- src/generator/common.rs | 2 +- src/main.rs | 10 ++++++---- src/parser/asyncapi_model_parser/validator.rs | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/generator/common.rs b/src/generator/common.rs index ce65762..8773500 100644 --- a/src/generator/common.rs +++ b/src/generator/common.rs @@ -21,7 +21,7 @@ macro_rules! cargo_command { /// checks if project with name already exists, if yes asks for permission to overwrite pub fn check_for_overwrite(output_path: &Path, project_title: &str) { if output_path.exists() { - println!("A project with the name {} already exists in the current directory, do you want to overwrite the existing project? \nWARNING: This will delete all files in the directory and all applied. \nType 'y' to continue or anything else to exit.", project_title); + println!("\nA project with the name {} already exists in the current directory: {}. Do you want to overwrite it? \n\nā— WARNING: This will permanently delete all files in the directory! \n\nType 'y' to continue or anything else to exit.", project_title, output_path.to_string_lossy()); let mut input = String::new(); match std::io::stdin().read_line(&mut input) { Ok(_) => { diff --git a/src/main.rs b/src/main.rs index 938714d..c2e1812 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,10 +27,7 @@ fn main() { let template_path = Path::new("./templates/"); let spec: AsyncAPI = match parser::asyncapi_model_parser::parse_spec_to_model(specfile_path) { - Ok(spec) => { - println!("šŸŽ‰ Specification was parsed successfully!"); - spec - } + Ok(spec) => spec, Err(e) => { eprintln!("āŒ Error parsing the specification: {}", e); std::process::exit(1); @@ -102,4 +99,9 @@ fn main() { println!("šŸ“š Generating docs..."); cargo_command!(output_path, "doc", "--no-deps"); } + + println!( + "šŸŽ‰ Generation finished!\n\n Run the service using:\n cd {} && cargo run\n\n If you are in the generator root, start the service using:\n just start-service {}\n", + output_path.to_string_lossy(), title.replace(' ', "_").to_lowercase() + ); } diff --git a/src/parser/asyncapi_model_parser/validator.rs b/src/parser/asyncapi_model_parser/validator.rs index 371af82..99821bb 100644 --- a/src/parser/asyncapi_model_parser/validator.rs +++ b/src/parser/asyncapi_model_parser/validator.rs @@ -10,6 +10,6 @@ pub fn validate_asyncapi_schema(validator: &serde_json::Value, instance: &serde_ } panic!("Validation failed"); } else { - println!("āœ… Validation succeeded!"); + println!("āœ… Specification valid!"); } }