Skip to main content

torrust_tracker_deployer_lib/application/services/rendering/
grafana.rs

1//! Grafana Template Rendering Service
2//!
3//! This service is responsible for rendering Grafana provisioning templates.
4//! It's used by multiple contexts (render command, release steps) to prepare
5//! Grafana datasource and dashboard configurations.
6
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use thiserror::Error;
11use tracing::info;
12
13use crate::domain::prometheus::PrometheusConfig;
14use crate::domain::template::TemplateManager;
15use crate::infrastructure::templating::grafana::template::renderer::{
16    GrafanaProjectGenerator, GrafanaProjectGeneratorError,
17};
18use crate::shared::Clock;
19
20/// Errors that can occur during Grafana template rendering
21#[derive(Error, Debug)]
22pub enum GrafanaTemplateRenderingServiceError {
23    /// Template rendering failed
24    #[error("Failed to render Grafana templates: {reason}")]
25    RenderingFailed {
26        /// Detailed reason for the failure
27        reason: String,
28    },
29}
30
31impl From<GrafanaProjectGeneratorError> for GrafanaTemplateRenderingServiceError {
32    fn from(error: GrafanaProjectGeneratorError) -> Self {
33        Self::RenderingFailed {
34            reason: error.to_string(),
35        }
36    }
37}
38
39/// Service for rendering Grafana provisioning templates
40///
41/// This service encapsulates the logic for rendering Grafana datasource and
42/// dashboard configuration files. It's designed to be shared across command
43/// handlers and steps that need to prepare Grafana provisioning.
44pub struct GrafanaTemplateRenderingService {
45    build_dir: PathBuf,
46    template_manager: Arc<TemplateManager>,
47    clock: Arc<dyn Clock>,
48}
49
50impl GrafanaTemplateRenderingService {
51    /// Build a `GrafanaTemplateRenderingService` from environment paths
52    ///
53    /// # Arguments
54    ///
55    /// * `templates_dir` - Directory containing the source templates
56    /// * `build_dir` - Directory where rendered templates will be written
57    /// * `clock` - The clock for generating timestamps
58    ///
59    /// # Returns
60    ///
61    /// Returns a configured `GrafanaTemplateRenderingService` ready for template rendering
62    #[must_use]
63    pub fn from_paths(templates_dir: PathBuf, build_dir: PathBuf, clock: Arc<dyn Clock>) -> Self {
64        let template_manager = Arc::new(TemplateManager::new(templates_dir));
65
66        Self {
67            build_dir,
68            template_manager,
69            clock,
70        }
71    }
72
73    /// Render Grafana provisioning templates
74    ///
75    /// This renders Grafana datasource and dashboard provisioning files to the build directory.
76    /// Returns `None` if Grafana or Prometheus is not configured (Prometheus is required as datasource).
77    ///
78    /// # Arguments
79    ///
80    /// * `grafana_configured` - Whether Grafana is configured in user inputs
81    /// * `prometheus_config` - Prometheus configuration (required for datasource, optional)
82    ///
83    /// # Returns
84    ///
85    /// Returns the path to the rendered Grafana provisioning directory, or `None` if not configured
86    ///
87    /// # Errors
88    ///
89    /// Returns `GrafanaTemplateRenderingServiceError::RenderingFailed` if template rendering fails.
90    pub fn render(
91        &self,
92        grafana_configured: bool,
93        prometheus_config: Option<&PrometheusConfig>,
94    ) -> Result<Option<PathBuf>, GrafanaTemplateRenderingServiceError> {
95        if !grafana_configured {
96            info!(
97                reason = "grafana_not_configured",
98                "Skipping Grafana template rendering - not configured"
99            );
100            return Ok(None);
101        }
102
103        let Some(prometheus_config) = prometheus_config else {
104            info!(
105                reason = "prometheus_not_configured",
106                "Skipping Grafana template rendering - Prometheus datasource requires Prometheus to be configured"
107            );
108            return Ok(None);
109        };
110
111        info!(
112            templates_dir = %self.template_manager.templates_dir().display(),
113            build_dir = %self.build_dir.display(),
114            "Rendering Grafana provisioning templates"
115        );
116
117        let generator = GrafanaProjectGenerator::new(
118            &self.build_dir,
119            self.template_manager.clone(),
120            self.clock.clone(),
121        );
122
123        generator.render(prometheus_config)?;
124
125        let grafana_provisioning_dir = self.build_dir.join("storage/grafana/provisioning");
126
127        info!(
128            grafana_provisioning_dir = %grafana_provisioning_dir.display(),
129            "Grafana provisioning templates rendered successfully"
130        );
131
132        Ok(Some(grafana_provisioning_dir))
133    }
134}