Skip to content

Commit 731eaf4

Browse files
committed
feat: [#238] implement Prometheus template renderer
- Create PrometheusConfigRenderer to load and render prometheus.yml.tera - Add PrometheusTemplate wrapper for Tera integration - Implement PrometheusProjectGenerator to orchestrate rendering - Extract context from PrometheusConfig and TrackerConfig - Add 12 unit tests with comprehensive coverage - All linters passing (markdown, yaml, toml, cspell, clippy, rustfmt, shellcheck)
1 parent 92aab59 commit 731eaf4

File tree

8 files changed

+830
-1
lines changed

8 files changed

+830
-1
lines changed

docs/issues/238-prometheus-slice-release-run-commands.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,34 @@ This task adds Prometheus as a metrics collection service for the Torrust Tracke
1717
- [ ] Allow users to disable Prometheus by removing its configuration section
1818
- [ ] Deploy and verify Prometheus collects metrics from tracker
1919

20+
## Progress
21+
22+
-**Phase 1**: Template Structure & Data Flow Design (commit: 2ca0fa9)
23+
24+
- Created `PrometheusContext` struct with `scrape_interval`, `api_token`, `api_port` fields
25+
- Implemented module structure following existing patterns
26+
- Added comprehensive unit tests (5 tests)
27+
- Created `templates/prometheus/prometheus.yml.tera` template
28+
29+
-**Phase 2**: Environment Configuration (commit: 92aab59)
30+
31+
- Created `PrometheusConfig` domain struct in `src/domain/prometheus/`
32+
- Added optional `prometheus` field to `UserInputs` (enabled by default)
33+
- Implemented comprehensive unit tests (5 tests)
34+
- Updated all constructors and test fixtures
35+
36+
- 🚧 **Phase 3**: Prometheus Template Renderer (in progress)
37+
38+
- Create `PrometheusProjectGenerator` implementation
39+
- Integrate with template rendering system
40+
- Wire up data flow: UserInputs → PrometheusContext → Template
41+
42+
-**Phase 4**: Docker Compose Integration (pending)
43+
-**Phase 5**: Release Command Implementation (pending)
44+
-**Phase 6**: Ansible Playbook Integration (pending)
45+
-**Phase 7**: Testing (pending)
46+
-**Phase 8**: Documentation (pending)
47+
2048
## 🏗️ Architecture Requirements
2149

2250
**DDD Layers**: Infrastructure + Domain

src/infrastructure/templating/prometheus/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
pub mod template;
1111

12-
pub use template::PrometheusContext;
12+
pub use template::{PrometheusContext, PrometheusProjectGenerator};
1313

1414
/// Subdirectory name for Prometheus-related files within the build directory.
1515
///

src/infrastructure/templating/prometheus/template/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//! This module provides template-related functionality for Prometheus configuration,
44
//! including wrappers for dynamic templates.
55
6+
pub mod renderer;
67
pub mod wrapper;
78

9+
pub use renderer::{PrometheusConfigRenderer, PrometheusProjectGenerator};
810
pub use wrapper::PrometheusContext;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! Template rendering for Prometheus configuration
2+
3+
pub mod project_generator;
4+
pub mod prometheus_config;
5+
6+
pub use project_generator::{PrometheusProjectGenerator, PrometheusProjectGeneratorError};
7+
pub use prometheus_config::{PrometheusConfigRenderer, PrometheusConfigRendererError};
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
//! Prometheus Project Generator
2+
//!
3+
//! Orchestrates the rendering of all Prometheus configuration templates following
4+
//! the Project Generator pattern.
5+
//!
6+
//! ## Architecture
7+
//!
8+
//! This follows the three-layer Project Generator pattern:
9+
//! - **Context** (`PrometheusContext`) - Defines variables needed by templates
10+
//! - **Template** (`PrometheusTemplate`) - Wraps template file with context
11+
//! - **Renderer** (`PrometheusConfigRenderer`) - Renders specific .tera templates
12+
//! - **`ProjectGenerator`** (this file) - Orchestrates all renderers
13+
//!
14+
//! ## Data Flow
15+
//!
16+
//! Environment Config → `PrometheusConfig` → `PrometheusContext` → Template Rendering
17+
18+
use std::path::{Path, PathBuf};
19+
use std::sync::Arc;
20+
21+
use thiserror::Error;
22+
use tracing::instrument;
23+
24+
use crate::domain::prometheus::PrometheusConfig;
25+
use crate::domain::template::TemplateManager;
26+
use crate::domain::tracker::TrackerConfig;
27+
use crate::infrastructure::templating::prometheus::template::{
28+
renderer::{PrometheusConfigRenderer, PrometheusConfigRendererError},
29+
PrometheusContext,
30+
};
31+
32+
/// Errors that can occur during Prometheus project generation
33+
#[derive(Error, Debug)]
34+
pub enum PrometheusProjectGeneratorError {
35+
/// Failed to create the build directory
36+
#[error("Failed to create build directory '{directory}': {source}")]
37+
DirectoryCreationFailed {
38+
directory: String,
39+
#[source]
40+
source: std::io::Error,
41+
},
42+
43+
/// Failed to render Prometheus configuration
44+
#[error("Failed to render Prometheus configuration: {0}")]
45+
RendererFailed(#[from] PrometheusConfigRendererError),
46+
47+
/// Missing required tracker configuration
48+
#[error("Tracker configuration is required to extract API token and port for Prometheus")]
49+
MissingTrackerConfig,
50+
}
51+
52+
/// Orchestrates Prometheus configuration template rendering
53+
///
54+
/// This is the Project Generator that coordinates all Prometheus template rendering.
55+
/// It follows the standard pattern:
56+
/// 1. Create build directory structure
57+
/// 2. Extract data from tracker and Prometheus configs
58+
/// 3. Build `PrometheusContext`
59+
/// 4. Call `PrometheusConfigRenderer` to render prometheus.yml.tera
60+
pub struct PrometheusProjectGenerator {
61+
build_dir: PathBuf,
62+
prometheus_renderer: PrometheusConfigRenderer,
63+
}
64+
65+
impl PrometheusProjectGenerator {
66+
/// Default relative path for Prometheus configuration files
67+
const PROMETHEUS_BUILD_PATH: &'static str = "storage/prometheus/etc";
68+
69+
/// Creates a new Prometheus project generator
70+
///
71+
/// # Arguments
72+
///
73+
/// * `build_dir` - The destination directory where templates will be rendered
74+
/// * `template_manager` - The template manager to source templates from
75+
#[must_use]
76+
pub fn new<P: AsRef<Path>>(build_dir: P, template_manager: Arc<TemplateManager>) -> Self {
77+
let prometheus_renderer = PrometheusConfigRenderer::new(template_manager);
78+
79+
Self {
80+
build_dir: build_dir.as_ref().to_path_buf(),
81+
prometheus_renderer,
82+
}
83+
}
84+
85+
/// Renders Prometheus configuration templates to the build directory
86+
///
87+
/// This method:
88+
/// 1. Creates the build directory structure for Prometheus config
89+
/// 2. Extracts API token and port from tracker configuration
90+
/// 3. Builds `PrometheusContext` with `scrape_interval`, `api_token`, `api_port`
91+
/// 4. Renders prometheus.yml.tera template
92+
/// 5. Writes the rendered content to prometheus.yml
93+
///
94+
/// # Arguments
95+
///
96+
/// * `prometheus_config` - Prometheus configuration (`scrape_interval`)
97+
/// * `tracker_config` - Tracker configuration (needed for API token and port)
98+
///
99+
/// # Errors
100+
///
101+
/// Returns an error if:
102+
/// - Tracker configuration is not provided
103+
/// - Build directory creation fails
104+
/// - Template loading fails
105+
/// - Template rendering fails
106+
/// - Writing output file fails
107+
#[instrument(
108+
name = "prometheus_project_generator_render",
109+
skip(self, prometheus_config, tracker_config),
110+
fields(
111+
build_dir = %self.build_dir.display()
112+
)
113+
)]
114+
pub fn render(
115+
&self,
116+
prometheus_config: &PrometheusConfig,
117+
tracker_config: &TrackerConfig,
118+
) -> Result<(), PrometheusProjectGeneratorError> {
119+
// Create build directory for Prometheus templates
120+
let prometheus_build_dir = self.build_dir.join(Self::PROMETHEUS_BUILD_PATH);
121+
std::fs::create_dir_all(&prometheus_build_dir).map_err(|source| {
122+
PrometheusProjectGeneratorError::DirectoryCreationFailed {
123+
directory: prometheus_build_dir.display().to_string(),
124+
source,
125+
}
126+
})?;
127+
128+
// Build PrometheusContext from configurations
129+
let context = Self::build_context(prometheus_config, tracker_config);
130+
131+
// Render prometheus.yml using PrometheusConfigRenderer
132+
self.prometheus_renderer
133+
.render(&context, &prometheus_build_dir)?;
134+
135+
Ok(())
136+
}
137+
138+
/// Builds `PrometheusContext` from Prometheus and Tracker configurations
139+
///
140+
/// # Arguments
141+
///
142+
/// * `prometheus_config` - Contains `scrape_interval`
143+
/// * `tracker_config` - Contains HTTP API `admin_token` and `bind_address`
144+
///
145+
/// # Returns
146+
///
147+
/// A `PrometheusContext` with:
148+
/// - `scrape_interval`: From `prometheus_config.scrape_interval`
149+
/// - `api_token`: From `tracker_config.http_api.admin_token`
150+
/// - `api_port`: Parsed from `tracker_config.http_api.bind_address`
151+
fn build_context(
152+
prometheus_config: &PrometheusConfig,
153+
tracker_config: &TrackerConfig,
154+
) -> PrometheusContext {
155+
let scrape_interval = prometheus_config.scrape_interval;
156+
let api_token = tracker_config.http_api.admin_token.clone();
157+
158+
// Extract port from SocketAddr
159+
let api_port = tracker_config.http_api.bind_address.port();
160+
161+
PrometheusContext::new(scrape_interval, api_token, api_port)
162+
}
163+
}
164+
165+
#[cfg(test)]
166+
mod tests {
167+
use std::fs;
168+
169+
use super::*;
170+
use crate::domain::tracker::HttpApiConfig;
171+
172+
fn create_test_template_manager() -> Arc<TemplateManager> {
173+
use tempfile::TempDir;
174+
175+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
176+
let templates_dir = temp_dir.path().join("templates");
177+
let prometheus_dir = templates_dir.join("prometheus");
178+
179+
fs::create_dir_all(&prometheus_dir).expect("Failed to create prometheus dir");
180+
181+
let template_content = r#"global:
182+
scrape_interval: {{ scrape_interval }}s
183+
184+
scrape_configs:
185+
- job_name: "tracker_stats"
186+
metrics_path: "/api/v1/stats"
187+
params:
188+
token: ["{{ api_token }}"]
189+
format: ["prometheus"]
190+
static_configs:
191+
- targets: ["tracker:{{ api_port }}"]
192+
"#;
193+
194+
fs::write(prometheus_dir.join("prometheus.yml.tera"), template_content)
195+
.expect("Failed to write template");
196+
197+
// Prevent temp_dir from being dropped
198+
std::mem::forget(temp_dir);
199+
200+
Arc::new(TemplateManager::new(templates_dir))
201+
}
202+
203+
fn create_test_tracker_config() -> TrackerConfig {
204+
TrackerConfig {
205+
http_api: HttpApiConfig {
206+
bind_address: "0.0.0.0:1212".parse().expect("valid address"),
207+
admin_token: "test_admin_token".to_string(),
208+
},
209+
..Default::default()
210+
}
211+
}
212+
213+
#[test]
214+
fn it_should_create_prometheus_build_directory() {
215+
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
216+
let build_dir = temp_dir.path().join("build");
217+
218+
let template_manager = create_test_template_manager();
219+
let generator = PrometheusProjectGenerator::new(&build_dir, template_manager);
220+
221+
let prometheus_config = PrometheusConfig::default();
222+
let tracker_config = create_test_tracker_config();
223+
224+
generator
225+
.render(&prometheus_config, &tracker_config)
226+
.expect("Failed to render templates");
227+
228+
let prometheus_dir = build_dir.join("storage/prometheus/etc");
229+
assert!(
230+
prometheus_dir.exists(),
231+
"Prometheus build directory should be created"
232+
);
233+
assert!(
234+
prometheus_dir.is_dir(),
235+
"Prometheus build path should be a directory"
236+
);
237+
}
238+
239+
#[test]
240+
fn it_should_render_prometheus_yml_with_default_config() {
241+
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
242+
let build_dir = temp_dir.path().join("build");
243+
244+
let template_manager = create_test_template_manager();
245+
let generator = PrometheusProjectGenerator::new(&build_dir, template_manager);
246+
247+
let prometheus_config = PrometheusConfig::default(); // scrape_interval: 15
248+
let tracker_config = create_test_tracker_config();
249+
250+
generator
251+
.render(&prometheus_config, &tracker_config)
252+
.expect("Failed to render templates");
253+
254+
let prometheus_yml_path = build_dir.join("storage/prometheus/etc/prometheus.yml");
255+
assert!(
256+
prometheus_yml_path.exists(),
257+
"prometheus.yml should be created"
258+
);
259+
260+
let content =
261+
fs::read_to_string(&prometheus_yml_path).expect("Failed to read prometheus.yml");
262+
263+
// Verify default values
264+
assert!(content.contains("scrape_interval: 15s"));
265+
assert!(content.contains(r#"token: ["test_admin_token"]"#));
266+
assert!(content.contains("targets: [\"tracker:1212\"]"));
267+
}
268+
269+
#[test]
270+
fn it_should_render_prometheus_yml_with_custom_scrape_interval() {
271+
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
272+
let build_dir = temp_dir.path().join("build");
273+
274+
let template_manager = create_test_template_manager();
275+
let generator = PrometheusProjectGenerator::new(&build_dir, template_manager);
276+
277+
let prometheus_config = PrometheusConfig {
278+
scrape_interval: 30,
279+
};
280+
let tracker_config = create_test_tracker_config();
281+
282+
generator
283+
.render(&prometheus_config, &tracker_config)
284+
.expect("Failed to render templates");
285+
286+
let content = fs::read_to_string(build_dir.join("storage/prometheus/etc/prometheus.yml"))
287+
.expect("Failed to read file");
288+
289+
assert!(content.contains("scrape_interval: 30s"));
290+
}
291+
292+
#[test]
293+
fn it_should_extract_api_port_from_tracker_config() {
294+
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
295+
let build_dir = temp_dir.path().join("build");
296+
297+
let template_manager = create_test_template_manager();
298+
let generator = PrometheusProjectGenerator::new(&build_dir, template_manager);
299+
300+
let prometheus_config = PrometheusConfig::default();
301+
let mut tracker_config = create_test_tracker_config();
302+
tracker_config.http_api.bind_address = "0.0.0.0:8080".parse().expect("valid address");
303+
304+
generator
305+
.render(&prometheus_config, &tracker_config)
306+
.expect("Failed to render templates");
307+
308+
let content = fs::read_to_string(build_dir.join("storage/prometheus/etc/prometheus.yml"))
309+
.expect("Failed to read file");
310+
311+
assert!(content.contains("targets: [\"tracker:8080\"]"));
312+
}
313+
314+
#[test]
315+
fn it_should_use_tracker_api_token() {
316+
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
317+
let build_dir = temp_dir.path().join("build");
318+
319+
let template_manager = create_test_template_manager();
320+
let generator = PrometheusProjectGenerator::new(&build_dir, template_manager);
321+
322+
let prometheus_config = PrometheusConfig::default();
323+
let mut tracker_config = create_test_tracker_config();
324+
tracker_config.http_api.admin_token = "custom_admin_token_123".to_string();
325+
326+
generator
327+
.render(&prometheus_config, &tracker_config)
328+
.expect("Failed to render templates");
329+
330+
let content = fs::read_to_string(build_dir.join("storage/prometheus/etc/prometheus.yml"))
331+
.expect("Failed to read file");
332+
333+
assert!(content.contains(r#"token: ["custom_admin_token_123"]"#));
334+
}
335+
}

0 commit comments

Comments
 (0)