Skip to content

Commit 22790de

Browse files
committed
feat: [#238] integrate Prometheus with Docker Compose
- Add prometheus_config field to DockerComposeContext - Implement with_prometheus() builder method - Add conditional Prometheus service to docker-compose.yml.tera - Use bind mount for Prometheus config: ./storage/prometheus/etc:/etc/prometheus:Z - Add 4 unit tests for Prometheus service rendering (with/without config) - All linters passing
1 parent 731eaf4 commit 22790de

File tree

4 files changed

+231
-5
lines changed

4 files changed

+231
-5
lines changed

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,24 @@ This task adds Prometheus as a metrics collection service for the Torrust Tracke
3333
- Implemented comprehensive unit tests (5 tests)
3434
- Updated all constructors and test fixtures
3535

36-
- 🚧 **Phase 3**: Prometheus Template Renderer (in progress)
36+
- **Phase 3**: Prometheus Template Renderer (commit: 731eaf4)
3737

38-
- Create `PrometheusProjectGenerator` implementation
39-
- Integrate with template rendering system
40-
- Wire up data flow: UserInputs → PrometheusContext → Template
38+
- Created `PrometheusConfigRenderer` to load and render `prometheus.yml.tera`
39+
- Implemented `PrometheusTemplate` wrapper for Tera integration
40+
- Created `PrometheusProjectGenerator` to orchestrate rendering workflow
41+
- Implemented context extraction from `PrometheusConfig` and `TrackerConfig`
42+
- Added 12 comprehensive unit tests with full coverage
43+
- All linters passing
44+
45+
-**Phase 4**: Docker Compose Integration (commit: pending)
46+
47+
- Added `prometheus_config: Option<PrometheusConfig>` field to `DockerComposeContext`
48+
- Implemented `with_prometheus()` method for context builder pattern
49+
- Added conditional Prometheus service to `docker-compose.yml.tera` template
50+
- Prometheus service uses bind mount: `./storage/prometheus/etc:/etc/prometheus:Z`
51+
- Added 4 comprehensive unit tests for Prometheus service rendering
52+
- All linters passing
4153

42-
-**Phase 4**: Docker Compose Integration (pending)
4354
-**Phase 5**: Release Command Implementation (pending)
4455
-**Phase 6**: Ansible Playbook Integration (pending)
4556
-**Phase 7**: Testing (pending)

src/infrastructure/templating/docker_compose/template/renderer/docker_compose.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,117 @@ mod tests {
337337
"Should not contain mysql_data volume"
338338
);
339339
}
340+
341+
#[test]
342+
fn it_should_render_prometheus_service_when_config_is_present() {
343+
use crate::domain::prometheus::PrometheusConfig;
344+
345+
let temp_dir = TempDir::new().unwrap();
346+
let template_manager = Arc::new(TemplateManager::new(temp_dir.path()));
347+
348+
let ports = TrackerPorts {
349+
udp_tracker_ports: vec![6868, 6969],
350+
http_tracker_ports: vec![7070],
351+
http_api_port: 1212,
352+
};
353+
let prometheus_config = PrometheusConfig {
354+
scrape_interval: 15,
355+
};
356+
let context = DockerComposeContext::new_sqlite(ports).with_prometheus(prometheus_config);
357+
358+
let renderer = DockerComposeRenderer::new(template_manager);
359+
let output_dir = TempDir::new().unwrap();
360+
361+
let result = renderer.render(&context, output_dir.path());
362+
assert!(
363+
result.is_ok(),
364+
"Rendering with Prometheus context should succeed"
365+
);
366+
367+
let output_path = output_dir.path().join("docker-compose.yml");
368+
let rendered_content = std::fs::read_to_string(&output_path)
369+
.expect("Should be able to read rendered docker-compose.yml");
370+
371+
// Verify Prometheus service is present
372+
assert!(
373+
rendered_content.contains("prometheus:"),
374+
"Rendered output should contain prometheus service"
375+
);
376+
assert!(
377+
rendered_content.contains("image: prom/prometheus:v3.0.1"),
378+
"Should use Prometheus v3.0.1 image"
379+
);
380+
assert!(
381+
rendered_content.contains("container_name: prometheus"),
382+
"Should set container name"
383+
);
384+
385+
// Verify port mapping
386+
assert!(
387+
rendered_content.contains("9090:9090"),
388+
"Should expose Prometheus port 9090"
389+
);
390+
391+
// Verify volume mount
392+
assert!(
393+
rendered_content.contains("./storage/prometheus/etc:/etc/prometheus:Z"),
394+
"Should mount Prometheus config directory"
395+
);
396+
397+
// Verify service dependency
398+
assert!(
399+
rendered_content.contains("depends_on:"),
400+
"Should have depends_on section"
401+
);
402+
assert!(
403+
rendered_content.contains("- tracker"),
404+
"Should depend on tracker"
405+
);
406+
407+
// Verify network
408+
assert!(
409+
rendered_content.contains("- backend_network"),
410+
"Should be on backend_network"
411+
);
412+
}
413+
414+
#[test]
415+
fn it_should_not_render_prometheus_service_when_config_is_absent() {
416+
let temp_dir = TempDir::new().unwrap();
417+
let template_manager = Arc::new(TemplateManager::new(temp_dir.path()));
418+
419+
let ports = TrackerPorts {
420+
udp_tracker_ports: vec![6868, 6969],
421+
http_tracker_ports: vec![7070],
422+
http_api_port: 1212,
423+
};
424+
let context = DockerComposeContext::new_sqlite(ports);
425+
426+
let renderer = DockerComposeRenderer::new(template_manager);
427+
let output_dir = TempDir::new().unwrap();
428+
429+
let result = renderer.render(&context, output_dir.path());
430+
assert!(
431+
result.is_ok(),
432+
"Rendering without Prometheus context should succeed"
433+
);
434+
435+
let output_path = output_dir.path().join("docker-compose.yml");
436+
let rendered_content = std::fs::read_to_string(&output_path)
437+
.expect("Should be able to read rendered docker-compose.yml");
438+
439+
// Verify Prometheus service is NOT present
440+
assert!(
441+
!rendered_content.contains("image: prom/prometheus:v3.0.1"),
442+
"Should not contain Prometheus service when config absent"
443+
);
444+
assert!(
445+
!rendered_content.contains("container_name: prometheus"),
446+
"Should not have prometheus container"
447+
);
448+
assert!(
449+
!rendered_content.contains("./storage/prometheus/etc:/etc/prometheus:Z"),
450+
"Should not have prometheus volume mount"
451+
);
452+
}
340453
}

src/infrastructure/templating/docker_compose/template/wrappers/docker_compose/context.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
66
use serde::Serialize;
77

8+
use crate::domain::prometheus::PrometheusConfig;
9+
810
/// Tracker port configuration
911
#[derive(Debug, Clone)]
1012
pub struct TrackerPorts {
@@ -54,6 +56,9 @@ pub struct DockerComposeContext {
5456
pub http_tracker_ports: Vec<u16>,
5557
/// HTTP API port
5658
pub http_api_port: u16,
59+
/// Prometheus configuration (optional)
60+
#[serde(skip_serializing_if = "Option::is_none")]
61+
pub prometheus_config: Option<PrometheusConfig>,
5762
}
5863

5964
impl DockerComposeContext {
@@ -86,6 +91,7 @@ impl DockerComposeContext {
8691
udp_tracker_ports: ports.udp_tracker_ports,
8792
http_tracker_ports: ports.http_tracker_ports,
8893
http_api_port: ports.http_api_port,
94+
prometheus_config: None,
8995
}
9096
}
9197

@@ -143,9 +149,21 @@ impl DockerComposeContext {
143149
udp_tracker_ports: ports.udp_tracker_ports,
144150
http_tracker_ports: ports.http_tracker_ports,
145151
http_api_port: ports.http_api_port,
152+
prometheus_config: None,
146153
}
147154
}
148155

156+
/// Add Prometheus configuration to the context
157+
///
158+
/// # Arguments
159+
///
160+
/// * `prometheus_config` - Prometheus configuration
161+
#[must_use]
162+
pub fn with_prometheus(mut self, prometheus_config: PrometheusConfig) -> Self {
163+
self.prometheus_config = Some(prometheus_config);
164+
self
165+
}
166+
149167
/// Get the database configuration
150168
#[must_use]
151169
pub fn database(&self) -> &DatabaseConfig {
@@ -169,6 +187,12 @@ impl DockerComposeContext {
169187
pub fn http_api_port(&self) -> u16 {
170188
self.http_api_port
171189
}
190+
191+
/// Get the Prometheus configuration if present
192+
#[must_use]
193+
pub fn prometheus_config(&self) -> Option<&PrometheusConfig> {
194+
self.prometheus_config.as_ref()
195+
}
172196
}
173197

174198
impl DatabaseConfig {
@@ -294,4 +318,62 @@ mod tests {
294318
let cloned = context.clone();
295319
assert_eq!(cloned.database().driver(), "mysql");
296320
}
321+
322+
#[test]
323+
fn it_should_not_include_prometheus_config_by_default() {
324+
let ports = TrackerPorts {
325+
udp_tracker_ports: vec![6868, 6969],
326+
http_tracker_ports: vec![7070],
327+
http_api_port: 1212,
328+
};
329+
let context = DockerComposeContext::new_sqlite(ports);
330+
331+
assert!(context.prometheus_config().is_none());
332+
}
333+
334+
#[test]
335+
fn it_should_include_prometheus_config_when_added() {
336+
let ports = TrackerPorts {
337+
udp_tracker_ports: vec![6868, 6969],
338+
http_tracker_ports: vec![7070],
339+
http_api_port: 1212,
340+
};
341+
let prometheus_config = PrometheusConfig {
342+
scrape_interval: 30,
343+
};
344+
let context = DockerComposeContext::new_sqlite(ports).with_prometheus(prometheus_config);
345+
346+
assert!(context.prometheus_config().is_some());
347+
assert_eq!(context.prometheus_config().unwrap().scrape_interval, 30);
348+
}
349+
350+
#[test]
351+
fn it_should_not_serialize_prometheus_config_when_absent() {
352+
let ports = TrackerPorts {
353+
udp_tracker_ports: vec![6868, 6969],
354+
http_tracker_ports: vec![7070],
355+
http_api_port: 1212,
356+
};
357+
let context = DockerComposeContext::new_sqlite(ports);
358+
359+
let serialized = serde_json::to_string(&context).unwrap();
360+
assert!(!serialized.contains("prometheus_config"));
361+
}
362+
363+
#[test]
364+
fn it_should_serialize_prometheus_config_when_present() {
365+
let ports = TrackerPorts {
366+
udp_tracker_ports: vec![6868, 6969],
367+
http_tracker_ports: vec![7070],
368+
http_api_port: 1212,
369+
};
370+
let prometheus_config = PrometheusConfig {
371+
scrape_interval: 20,
372+
};
373+
let context = DockerComposeContext::new_sqlite(ports).with_prometheus(prometheus_config);
374+
375+
let serialized = serde_json::to_string(&context).unwrap();
376+
assert!(serialized.contains("prometheus_config"));
377+
assert!(serialized.contains("\"scrape_interval\":20"));
378+
}
297379
}

templates/docker-compose/docker-compose.yml.tera

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ services:
5757
max-size: "10m"
5858
max-file: "10"
5959

60+
{% if prometheus_config %}
61+
prometheus:
62+
image: prom/prometheus:v3.0.1
63+
container_name: prometheus
64+
tty: true
65+
restart: unless-stopped
66+
networks:
67+
- backend_network
68+
ports:
69+
- "9090:9090"
70+
volumes:
71+
- ./storage/prometheus/etc:/etc/prometheus:Z
72+
logging:
73+
options:
74+
max-size: "10m"
75+
max-file: "10"
76+
depends_on:
77+
- tracker
78+
{% endif %}
79+
6080
{% if database.driver == "mysql" %}
6181
mysql:
6282
image: mysql:8.0

0 commit comments

Comments
 (0)