diff --git a/.gitignore b/.gitignore index 56ee30e..65db440 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Ignora directorios de compilación **/target +# Archivos de log +**/log/*.log* + # Archivos de configuración locales **/local.*.toml **/local.toml diff --git a/Cargo.lock b/Cargo.lock index 045a72f..43eb210 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,6 +287,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "bytes" version = "1.10.1" @@ -369,6 +375,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.6" @@ -737,12 +758,28 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "language-tags" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.174" @@ -794,6 +831,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.5" @@ -827,6 +873,22 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -848,9 +910,15 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pagetop" -version = "0.0.2" +version = "0.0.3" dependencies = [ "actix-web", "colored", @@ -860,6 +928,10 @@ dependencies = [ "serde", "substring", "terminal_size", + "tracing", + "tracing-actix-web", + "tracing-appender", + "tracing-subscriber", ] [[package]] @@ -904,6 +976,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1016,8 +1108,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1028,7 +1129,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] [[package]] @@ -1037,6 +1138,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -1062,6 +1169,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.20" @@ -1138,6 +1251,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1222,6 +1344,35 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -1339,6 +1490,31 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-actix-web" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2340b7722695166c7fc9b3e3cd1166e7c74fedb9075b8f0c74d3822d2e41caf5" +dependencies = [ + "actix-web", + "mutually_exclusive_features", + "pin-project", + "tracing", + "uuid", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" @@ -1357,6 +1533,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -1394,6 +1613,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -1415,6 +1651,86 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index a844f52..f5948d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pagetop" -version = "0.0.2" +version = "0.0.3" edition = "2021" description = """\ @@ -22,6 +22,11 @@ serde.workspace = true substring = "1.4.5" terminal_size = "0.4.2" +tracing = "0.1.41" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.19", features = ["json", "env-filter"] } +tracing-actix-web = "0.7.18" + actix-web = "4.11.0" pagetop-macros.workspace = true diff --git a/src/app.rs b/src/app.rs index bac7482..d21c08a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,11 +2,12 @@ mod figfont; -use crate::{global, service}; +use crate::{global, service, trace}; use substring::Substring; use std::io::Error; +use std::sync::LazyLock; pub struct Application; @@ -15,6 +16,10 @@ impl Application { pub fn new() -> Self { // Al arrancar muestra una cabecera para la aplicación. Self::show_banner(); + + // Inicia gestión de trazas y registro de eventos (logging). + LazyLock::force(&trace::TRACING); + Self } @@ -62,13 +67,15 @@ impl Application { /// Ejecuta el servidor web de la aplicación. pub fn run(self) -> Result { // Prepara el servidor web. - Ok(service::HttpServer::new(move || Self::service_app()) - .bind(format!( - "{}:{}", - &global::SETTINGS.server.bind_address, - &global::SETTINGS.server.bind_port - ))? - .run()) + Ok(service::HttpServer::new(move || { + Self::service_app().wrap(tracing_actix_web::TracingLogger::default()) + }) + .bind(format!( + "{}:{}", + &global::SETTINGS.server.bind_address, + &global::SETTINGS.server.bind_port + ))? + .run()) } /// Prepara el servidor web de la aplicación para pruebas. diff --git a/src/global.rs b/src/global.rs index 5e3d52f..8432032 100644 --- a/src/global.rs +++ b/src/global.rs @@ -10,48 +10,65 @@ include_config!(SETTINGS: Settings => [ "app.description" => "Developed with the amazing PageTop framework.", "app.startup_banner" => "Slant", + // [log] + "log.tracing" => "Info", + "log.rolling" => "Stdout", + "log.path" => "log", + "log.prefix" => "tracing.log", + "log.format" => "Full", + // [server] "server.bind_address" => "localhost", "server.bind_port" => 8080, ]); #[derive(Debug, Deserialize)] -/// Ajustes de configuración para las secciones globales [`[app]`](App) y [`[server]`](Server). -/// Consulta [`SETTINGS`] para los valores por defecto. +/// Ajustes para las secciones globales [`[app]`](App), [`[log]`](Log) y [`[server]`](Server) de +/// [`SETTINGS`]. pub struct Settings { pub app: App, + pub log: Log, pub server: Server, } #[derive(Debug, Deserialize)] -/// Sección `[app]` de la configuración. -/// -/// Forma parte de [`Settings`]. +/// Sección `[app]` de la configuración. Forma parte de [`Settings`]. pub struct App { /// Nombre de la aplicación. - /// Valor por defecto: *"Sample"*. pub name: String, /// Breve descripción de la aplicación. - /// Valor por defecto: *"Developed with the amazing PageTop framework."*. pub description: String, - /// ASCII banner printed at startup: *"Off"*, *"Slant"*, *"Small"*, *"Speed"*, or *"Starwars"*. - /// Default: *"Slant"*. + /// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o + /// *"Starwars"*. pub startup_banner: String, - /// Modo de ejecución. - /// Valor por defecto: el definido por la variable de entorno - /// `PAGETOP_RUN_MODE`, o *"default"* si no está establecida. + /// Modo de ejecución, dado por la variable de entorno `PAGETOP_RUN_MODE`, o *"default"* si no + /// está definido. pub run_mode: String, } #[derive(Debug, Deserialize)] -/// Sección `[server]` de la configuración. -/// -/// Forma parte de [`Settings`]. +/// Sección `[log]` de la configuración. Forma parte de [`Settings`]. +pub struct Log { + /// Opciones, o combinación de opciones separadas por comas, para filtrar las trazas: *"Error"*, + /// *"Warn"*, *"Info"*, *"Debug"* o *"Trace"*. + /// Ejemplo: "Error,actix_server::builder=Info,tracing_actix_web=Debug". + pub tracing: String, + /// Muestra los mensajes de traza en el terminal (*"Stdout"*) o las registra en archivos con + /// rotación: *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*. + pub rolling: String, + /// Directorio para los archivos de traza (si `rolling` ≠ *"Stdout"*). + pub path: String, + /// Prefijo para los archivos de traza (si `rolling` ≠ *"Stdout"*). + pub prefix: String, + /// Formato de salida de las trazas. Opciones: *"Full"*, *"Compact"*, *"Pretty"* o *"Json"*. + pub format: String, +} + +#[derive(Debug, Deserialize)] +/// Sección `[server]` de la configuración. Forma parte de [`Settings`]. pub struct Server { /// Dirección de enlace para el servidor web. - /// Valor por defecto: *"localhost"*. pub bind_address: String, /// Puerto de escucha del servidor web. - /// Valor por defecto: *8088*. pub bind_port: u16, } diff --git a/src/lib.rs b/src/lib.rs index 81e7bc6..308c163 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,8 @@ pub use pagetop_macros::{main, test}; pub mod config; // Opciones de configuración globales. pub mod global; +// Gestión de trazas y registro de eventos de la aplicación. +pub mod trace; // Gestión del servidor y servicios web. pub mod service; // Prepara y ejecuta la aplicación. diff --git a/src/prelude.rs b/src/prelude.rs index bee222e..5d5851f 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -13,6 +13,8 @@ pub use crate::include_config; pub use crate::global; +pub use crate::trace; + pub use crate::service; pub use crate::app::Application; diff --git a/src/trace.rs b/src/trace.rs new file mode 100644 index 0000000..93a8ff2 --- /dev/null +++ b/src/trace.rs @@ -0,0 +1,85 @@ +//! Gestión de trazas y registro de eventos de la aplicación. +//! +//! `PageTop` recopila información de diagnóstico de la aplicación de forma estructurada y basada en +//! eventos. +//! +//! En los sistemas asíncronos, interpretar los mensajes de log tradicionales suele volverse +//! complicado. Las tareas individuales se multiplexan en el mismo hilo y los eventos y registros +//! asociados se entremezclan, lo que dificulta seguir la secuencia lógica. +//! +//! `PageTop` usa [`tracing`](https://docs.rs/tracing) para registrar eventos estructurados y con +//! información adicional sobre la *temporalidad* y la *causalidad*. A diferencia de un mensaje de +//! log, un *span* (intervalo) tiene un momento de inicio y de fin, puede entrar y salir del flujo +//! de ejecución y puede existir dentro de un árbol anidado de *spans* similares. Además, estos +//! *spans* son estructurados, con la capacidad de registrar tipos de datos y mensajes de texto. + +use crate::global; + +pub use tracing::{debug, error, info, trace, warn}; +pub use tracing::{debug_span, error_span, info_span, trace_span, warn_span}; + +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::EnvFilter; + +use std::sync::LazyLock; + +/// Trazado y registro de eventos de la aplicación. +/// +/// Para aumentar el rendimiento, un hilo dedicado utiliza un sistema de escritura no bloqueante que +/// actúa de forma periódica en lugar de enviar cada traza o evento al instante. Si el programa +/// termina abruptamente (por ejemplo, debido a un `panic!` o a una llamada a `std::process::exit`), +/// es posible que algunas trazas o eventos no se envíen. +/// +/// Dado que las trazas o eventos registrados poco antes de un fallo suelen ser cruciales para +/// diagnosticar la causa, `Lazy` garantiza que todos los registros almacenados se +/// envíen antes de finalizar la ejecución. + +#[rustfmt::skip] +pub(crate) static TRACING: LazyLock = LazyLock::new(|| { + let env_filter = EnvFilter::try_new(&global::SETTINGS.log.tracing) + .unwrap_or_else(|_| EnvFilter::new("Info")); + + let rolling = global::SETTINGS.log.rolling.to_lowercase(); + + let (non_blocking, guard) = match rolling.as_str() { + "stdout" => tracing_appender::non_blocking(std::io::stdout()), + _ => tracing_appender::non_blocking({ + let path = &global::SETTINGS.log.path; + let prefix = &global::SETTINGS.log.prefix; + match rolling.as_str() { + "daily" => tracing_appender::rolling::daily(path, prefix), + "hourly" => tracing_appender::rolling::hourly(path, prefix), + "minutely" => tracing_appender::rolling::minutely(path, prefix), + "endless" => tracing_appender::rolling::never(path, prefix), + _ => { + println!( + "Rolling value \"{}\" not valid. Using \"daily\". Check the settings file.", + global::SETTINGS.log.rolling, + ); + tracing_appender::rolling::daily(path, prefix) + } + } + }), + }; + + let subscriber = tracing_subscriber::fmt() + .with_env_filter(env_filter) + .with_writer(non_blocking) + .with_ansi(rolling.as_str() == "stdout"); + + match global::SETTINGS.log.format.to_lowercase().as_str() { + "json" => subscriber.json().init(), + "full" => subscriber.init(), + "compact" => subscriber.compact().init(), + "pretty" => subscriber.pretty().init(), + _ => { + println!( + "Tracing format \"{}\" not valid. Using \"Full\". Check the settings file.", + global::SETTINGS.log.format, + ); + subscriber.init(); + } + } + + guard +});