drogue_bazaar/app/run/
mod.rs

1mod main;
2
3pub use main::*;
4
5use crate::app::init::{self, Tracing};
6use crate::core::{config::ConfigFromEnv, info::ComponentInformation};
7use crate::{app::health::HealthServerConfig, core::Spawner, health::HealthChecked};
8use std::future::Future;
9use std::io::Write;
10use std::pin::Pin;
11use std::time::Duration;
12
13#[derive(Clone, Debug, Default, serde::Deserialize)]
14pub struct RuntimeConfig {
15    #[serde(default)]
16    pub console_metrics: ConsoleMetrics,
17    #[serde(default)]
18    pub health: HealthServerConfig,
19    #[serde(default)]
20    pub tracing: Tracing,
21}
22
23#[derive(Clone, Debug, serde::Deserialize)]
24pub struct ConsoleMetrics {
25    pub enabled: bool,
26    #[serde(
27        default = "default::console_metrics_duration",
28        with = "humantime_serde"
29    )]
30    pub period: Duration,
31}
32
33impl Default for ConsoleMetrics {
34    fn default() -> Self {
35        Self {
36            enabled: false,
37            period: default::console_metrics_duration(),
38        }
39    }
40}
41
42mod default {
43    use super::*;
44
45    pub const fn console_metrics_duration() -> Duration {
46        Duration::from_secs(60)
47    }
48}
49
50pub struct Runtime {
51    component: ComponentInformation,
52    dotenv: Option<bool>,
53    show_banner: Option<bool>,
54}
55
56/// Create a new runtime, using the local crate as component.
57///
58/// ```
59/// use drogue_bazaar::{project, runtime, app::{Main, Startup}};
60///
61/// project!(PROJECT: "Drogue IoT");
62///
63/// #[derive(serde::Deserialize)]
64/// struct Config {}
65///
66/// async fn run(config: Config, startup: &mut dyn Startup) -> anyhow::Result<()> {
67///     Ok(())
68/// }
69///
70/// fn main() {
71///     let runtime = runtime!(PROJECT)
72///         .exec(run);
73/// }
74/// ```
75#[macro_export]
76macro_rules! runtime {
77    ($project:expr) => {
78        $crate::app::Runtime::new($crate::component!($project))
79    };
80}
81
82impl Runtime {
83    pub fn new(component: ComponentInformation) -> Self {
84        Self {
85            component,
86            dotenv: None,
87            show_banner: None,
88        }
89    }
90
91    /// Force dotenv option.
92    ///
93    /// ```
94    /// use drogue_bazaar::{project, runtime};
95    ///
96    /// project!(PROJECT: "Drogue IoT");
97    ///
98    /// fn main() {
99    ///     runtime!(PROJECT)
100    ///         .dotenv(false);
101    /// }
102    /// ```
103    #[allow(clippy::needless_doctest_main)]
104    pub fn dotenv<I: Into<Option<bool>>>(mut self, dotenv: I) -> Self {
105        self.dotenv = dotenv.into();
106        self
107    }
108
109    /// Show the application banner
110    fn banner(&self) {
111        if self
112            .show_banner
113            .or_else(|| flag_opt("RUNTIME__SHOW_BANNER"))
114            .unwrap_or(true)
115        {
116            println!(
117                r#"{}  
118{} {} - {} {} ({})
119"#,
120                self.component.project.banner,
121                self.component.project.name,
122                self.component.project.version,
123                self.component.name,
124                self.component.version,
125                self.component.description
126            );
127
128            std::io::stdout().flush().ok();
129        }
130    }
131
132    pub async fn exec<C, A>(self, app: A) -> anyhow::Result<()>
133    where
134        A: App<C>,
135        for<'de> C: ConfigFromEnv<'de>,
136    {
137        // phase 1: early init, cannot really rely on env-vars, but may add its own
138
139        init::phase1(
140            self.dotenv
141                .unwrap_or_else(|| !flag("RUNTIME__DISABLE_DOTENV")),
142        );
143
144        // phase 2: Show early runtime information
145        self.banner();
146
147        // phase 3: env-vars are ready now, we can make use of them
148
149        let mut main = Main::from_env()?;
150        init::phase2(self.component.name, main.runtime_config().tracing.clone());
151
152        // phase 4: main app startup
153
154        let config = C::from_env()?;
155        app.run(config, &mut main).await?;
156        main.run().await?;
157
158        // exiting, shutdown tracing (flush)
159        opentelemetry::global::shutdown_tracer_provider();
160
161        // done
162
163        Ok(())
164    }
165
166    pub async fn exec_fn<C, F>(self, f: F) -> anyhow::Result<()>
167    where
168        for<'de> C: ConfigFromEnv<'de> + Send + 'static,
169        F: for<'f> AppFn<C, &'f mut dyn Startup>,
170    {
171        self.exec(f).await
172    }
173}
174
175pub trait AppFn<C, S>: FnOnce(C, S) -> <Self as AppFn<C, S>>::Fut {
176    type Fut: Future<Output = anyhow::Result<()>>;
177}
178
179impl<C, S, F, Fut> AppFn<C, S> for F
180where
181    F: FnOnce(C, S) -> Fut,
182    Fut: Future<Output = anyhow::Result<()>>,
183{
184    type Fut = Fut;
185}
186
187#[async_trait::async_trait(?Send)]
188pub trait App<C>
189where
190    for<'de> C: ConfigFromEnv<'de>,
191{
192    async fn run(self, config: C, startup: &mut dyn Startup) -> anyhow::Result<()>;
193}
194
195#[async_trait::async_trait(?Send)]
196impl<C, A> App<C> for A
197where
198    A: for<'f> AppFn<C, &'f mut dyn Startup>,
199    C: for<'de> ConfigFromEnv<'de> + 'static,
200{
201    async fn run(self, config: C, startup: &mut dyn Startup) -> anyhow::Result<()> {
202        (self)(config, startup).await
203    }
204}
205
206fn flag(name: &str) -> bool {
207    flag_opt(name).unwrap_or_default()
208}
209
210fn flag_opt(name: &str) -> Option<bool> {
211    std::env::var(name).map(|v| v.to_lowercase() == "true").ok()
212}
213
214/// Startup context.
215pub trait Startup: Spawner {
216    /// Add a health check.
217    fn check_boxed(&mut self, check: Box<dyn HealthChecked>);
218
219    /// Allow the application to check if the runtime wants to enable tracing.
220    ///
221    /// This can be used to e.g. add some tracing logger into the HTTP stack.
222    fn use_tracing(&self) -> bool;
223
224    /// Access the runtime config.
225    fn runtime_config(&self) -> &RuntimeConfig;
226}
227
228pub trait StartupExt: Startup {
229    /// Add several health checks at once.
230    fn check_iter<I>(&mut self, iter: I)
231    where
232        I: IntoIterator<Item = Box<dyn HealthChecked>>,
233    {
234        for i in iter {
235            self.check_boxed(i);
236        }
237    }
238
239    fn check<C>(&mut self, c: C)
240    where
241        C: HealthChecked + 'static,
242    {
243        self.check_boxed(Box::new(c))
244    }
245
246    fn spawn_iter<I>(&mut self, iter: I)
247    where
248        I: IntoIterator<Item = Pin<Box<dyn Future<Output = anyhow::Result<()>>>>>,
249    {
250        for i in iter {
251            self.spawn_boxed(i);
252        }
253    }
254
255    fn spawn<F>(&mut self, f: F)
256    where
257        F: Future<Output = anyhow::Result<()>> + 'static,
258    {
259        self.spawn_boxed(Box::pin(f))
260    }
261}
262
263impl<S: ?Sized> StartupExt for S where S: Startup {}