Skip to main content

lightshuttle_otel/
config.rs

1//! OpenTelemetry collector configuration.
2
3use std::collections::HashMap;
4
5use lightshuttle_runtime::{ContainerSpec, ImageSource, PortBinding};
6
7/// Resource name used for the bundled collector inside the lifecycle
8/// plan. Stable so dependents can refer to it via the standard
9/// `${resources.lightshuttle_otel.host}` interpolation if needed.
10pub const SYNTHETIC_RESOURCE_NAME: &str = "lightshuttle_otel";
11
12/// Default OTLP gRPC port (collector receiver).
13const DEFAULT_OTLP_GRPC_PORT: u16 = 4317;
14
15/// Default OTLP HTTP port (collector receiver).
16const DEFAULT_OTLP_HTTP_PORT: u16 = 4318;
17
18/// Default collector image, pinned to a known-good tag.
19const DEFAULT_IMAGE: &str = "otel/opentelemetry-collector:0.108.0";
20
21/// Strongly-typed configuration of the bundled OpenTelemetry collector.
22///
23/// All fields are public so callers can override individual knobs
24/// (image tag, port mapping) without recreating the whole value.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct CollectorConfig {
27    /// Container image of the collector.
28    pub image: String,
29    /// Host-side OTLP gRPC port published by the collector.
30    pub otlp_grpc_port: u16,
31    /// Host-side OTLP HTTP port published by the collector.
32    pub otlp_http_port: u16,
33}
34
35impl CollectorConfig {
36    /// Sane defaults: official upstream image, OTLP gRPC on `:4317`,
37    /// OTLP HTTP on `:4318`.
38    #[must_use]
39    pub fn defaults() -> Self {
40        Self {
41            image: DEFAULT_IMAGE.to_owned(),
42            otlp_grpc_port: DEFAULT_OTLP_GRPC_PORT,
43            otlp_http_port: DEFAULT_OTLP_HTTP_PORT,
44        }
45    }
46
47    /// Hostname that dependents must use to reach the collector from
48    /// inside the project network. Mirrors the
49    /// `<project>_<resource>` container name convention used by
50    /// `lightshuttle-runtime`.
51    #[must_use]
52    pub fn hostname(&self, project: &str) -> String {
53        format!("{project}_{SYNTHETIC_RESOURCE_NAME}")
54    }
55
56    /// Build a [`ContainerSpec`] runnable by `lightshuttle-runtime`.
57    ///
58    /// The collector is started in `--config=builtin:default-config`
59    /// mode and listens on the OTLP gRPC and HTTP ports defined by
60    /// this configuration.
61    #[must_use]
62    pub fn to_container_spec(&self, project: &str) -> ContainerSpec {
63        ContainerSpec {
64            name: format!("{project}_{SYNTHETIC_RESOURCE_NAME}"),
65            project: project.to_owned(),
66            resource: SYNTHETIC_RESOURCE_NAME.to_owned(),
67            image: ImageSource::Pull(self.image.clone()),
68            env: HashMap::new(),
69            ports: vec![
70                PortBinding {
71                    container_port: DEFAULT_OTLP_GRPC_PORT,
72                    host_address: Some("127.0.0.1".to_owned()),
73                    host_port: self.otlp_grpc_port,
74                },
75                PortBinding {
76                    container_port: DEFAULT_OTLP_HTTP_PORT,
77                    host_address: Some("127.0.0.1".to_owned()),
78                    host_port: self.otlp_http_port,
79                },
80            ],
81            volumes: Vec::new(),
82            command: None,
83            // No Docker healthcheck. The previous `... || exit 0` probe
84            // always reported healthy and masked a crashed collector. The
85            // collector image runs `--config=builtin:default-config`,
86            // which does not enable the health_check extension, so there
87            // is no reliable HTTP probe to target. A crash is instead
88            // surfaced through the container exit status: `wait_healthy`
89            // observes a stopped container and fails rather than passing
90            // a dead collector off as healthy.
91            healthcheck: None,
92            working_dir: None,
93        }
94    }
95}
96
97impl Default for CollectorConfig {
98    fn default() -> Self {
99        Self::defaults()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn defaults_match_otlp_standard_ports() {
109        let cfg = CollectorConfig::defaults();
110        assert_eq!(cfg.otlp_grpc_port, 4317);
111        assert_eq!(cfg.otlp_http_port, 4318);
112        assert!(cfg.image.starts_with("otel/opentelemetry-collector"));
113    }
114
115    #[test]
116    fn hostname_is_project_prefixed() {
117        let cfg = CollectorConfig::defaults();
118        assert_eq!(cfg.hostname("demo"), "demo_lightshuttle_otel");
119    }
120
121    #[test]
122    fn to_container_spec_publishes_both_otlp_ports() {
123        let cfg = CollectorConfig::defaults();
124        let spec = cfg.to_container_spec("demo");
125
126        assert_eq!(spec.name, "demo_lightshuttle_otel");
127        assert_eq!(spec.project, "demo");
128        assert_eq!(spec.resource, "lightshuttle_otel");
129        assert_eq!(spec.ports.len(), 2);
130        let host_ports: Vec<u16> = spec.ports.iter().map(|p| p.host_port).collect();
131        assert!(host_ports.contains(&4317));
132        assert!(host_ports.contains(&4318));
133    }
134}