opentelemetry_configuration/
rust_detector.rs

1//! Rust-specific resource detection.
2//!
3//! This module provides:
4//! - [`RustResourceDetector`] - Automatic detection of Rust runtime attributes
5//! - [`RustBuildInfo`] - Struct for build-time rustc information
6//! - [`emit_rustc_env`] - Build.rs helper to capture rustc version
7
8use opentelemetry::KeyValue;
9use opentelemetry_sdk::resource::{Resource, ResourceDetector};
10use opentelemetry_semantic_conventions::resource::{
11    PROCESS_RUNTIME_DESCRIPTION, PROCESS_RUNTIME_NAME, PROCESS_RUNTIME_VERSION,
12};
13
14/// Detects Rust runtime resource attributes.
15///
16/// Captures metadata available at runtime without requiring build.rs:
17/// - `process.runtime.name` = "rust" (semantic convention)
18/// - `rust.target_os`, `rust.target_arch`, `rust.target_family`
19/// - `rust.debug` (true for debug builds)
20/// - `process.executable.size` (binary size in bytes)
21///
22/// For rustc version and channel, use [`emit_rustc_env`] in build.rs combined
23/// with the [`capture_rust_build_info!`](crate::capture_rust_build_info) macro.
24pub struct RustResourceDetector;
25
26impl ResourceDetector for RustResourceDetector {
27    fn detect(&self) -> Resource {
28        let mut attrs = vec![
29            KeyValue::new(PROCESS_RUNTIME_NAME, "rust"),
30            KeyValue::new("rust.target_os", std::env::consts::OS),
31            KeyValue::new("rust.target_arch", std::env::consts::ARCH),
32            KeyValue::new("rust.target_family", std::env::consts::FAMILY),
33            KeyValue::new("rust.debug", cfg!(debug_assertions)),
34        ];
35
36        if let Ok(exe_path) = std::env::current_exe()
37            && let Ok(metadata) = std::fs::metadata(&exe_path)
38        {
39            let size = i64::try_from(metadata.len()).unwrap_or(i64::MAX);
40            attrs.push(KeyValue::new("process.executable.size", size));
41        }
42
43        Resource::builder().with_attributes(attrs).build()
44    }
45}
46
47/// Rust build-time information captured via build.rs.
48///
49/// Use [`emit_rustc_env`] in your build.rs and [`capture_rust_build_info!`](crate::capture_rust_build_info)
50/// in your application code to populate this struct.
51///
52/// # Example
53///
54/// In build.rs:
55///
56/// ```
57/// opentelemetry_configuration::emit_rustc_env();
58/// ```
59///
60/// In main.rs:
61///
62/// ```no_run
63/// # fn main() -> Result<(), opentelemetry_configuration::SdkError> {
64/// use opentelemetry_configuration::OtelSdkBuilder;
65///
66/// let _guard = OtelSdkBuilder::new()
67///     .service_name("my-service")
68///     .with_rust_build_info(opentelemetry_configuration::capture_rust_build_info!())
69///     .build()?;
70/// # Ok(())
71/// # }
72/// ```
73#[derive(Debug, Clone, Copy, Default)]
74pub struct RustBuildInfo {
75    /// Rustc version (e.g., "1.84.0").
76    pub rustc_version: Option<&'static str>,
77    /// Rust release channel ("stable", "beta", "nightly").
78    pub rust_channel: Option<&'static str>,
79    /// Full rustc version string (e.g., "rustc 1.84.0 (9fc6b4312 2024-01-04)").
80    pub rustc_version_full: Option<&'static str>,
81}
82
83impl RustBuildInfo {
84    /// Converts to OpenTelemetry `KeyValue` pairs for resource attributes.
85    ///
86    /// Returns attributes using semantic conventions where applicable:
87    /// - `process.runtime.version` for rustc version
88    /// - `process.runtime.description` for full version string
89    /// - `rust.channel` for release channel
90    #[must_use]
91    pub fn to_key_values(&self) -> Vec<KeyValue> {
92        let mut attrs = Vec::new();
93
94        if let Some(version) = self.rustc_version {
95            attrs.push(KeyValue::new(PROCESS_RUNTIME_VERSION, version));
96        }
97        if let Some(channel) = self.rust_channel {
98            attrs.push(KeyValue::new("rust.channel", channel));
99        }
100        if let Some(full) = self.rustc_version_full {
101            attrs.push(KeyValue::new(PROCESS_RUNTIME_DESCRIPTION, full));
102        }
103
104        attrs
105    }
106}
107
108/// Emits rustc version information as cargo environment variables.
109///
110/// Call this from your `build.rs` to capture rustc version at compile time.
111/// The emitted environment variables can then be read using the
112/// [`capture_rust_build_info!`](crate::capture_rust_build_info) macro.
113///
114/// # Environment Variables Emitted
115///
116/// - `RUSTC_VERSION` - The rustc version number (e.g., "1.84.0")
117/// - `RUSTC_VERSION_FULL` - Full version string (e.g., "rustc 1.84.0 (9fc6b4312 2024-01-04)")
118/// - `RUST_CHANNEL` - Release channel ("stable", "beta", or "nightly")
119///
120/// # Example
121///
122/// ```
123/// // In build.rs:
124/// opentelemetry_configuration::emit_rustc_env();
125/// ```
126pub fn emit_rustc_env() {
127    use std::process::Command;
128
129    println!("cargo::rerun-if-env-changed=RUSTC");
130
131    let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
132
133    if let Ok(output) = Command::new(&rustc).arg("--version").output()
134        && let Ok(version_str) = String::from_utf8(output.stdout)
135    {
136        let version_str = version_str.trim();
137        println!("cargo::rustc-env=RUSTC_VERSION_FULL={version_str}");
138
139        if let Some(version) = version_str.strip_prefix("rustc ")
140            && let Some(ver) = version.split_whitespace().next()
141        {
142            println!("cargo::rustc-env=RUSTC_VERSION={ver}");
143        }
144    }
145
146    if let Ok(output) = Command::new(&rustc).arg("-vV").output()
147        && let Ok(verbose) = String::from_utf8(output.stdout)
148    {
149        for line in verbose.lines() {
150            if let Some(release) = line.strip_prefix("release: ") {
151                let channel_name = if release.contains("nightly") {
152                    "nightly"
153                } else if release.contains("beta") {
154                    "beta"
155                } else {
156                    "stable"
157                };
158                println!("cargo::rustc-env=RUST_CHANNEL={channel_name}");
159                break;
160            }
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use opentelemetry_sdk::resource::ResourceDetector;
169
170    #[test]
171    fn test_rust_detector_includes_runtime_name() {
172        let detector = RustResourceDetector;
173        let resource = detector.detect();
174
175        let runtime_name = resource
176            .iter()
177            .find(|(k, _)| k.as_str() == PROCESS_RUNTIME_NAME);
178        assert!(runtime_name.is_some());
179    }
180
181    #[test]
182    fn test_rust_build_info_to_key_values_empty() {
183        let info = RustBuildInfo::default();
184        assert!(info.to_key_values().is_empty());
185    }
186
187    #[test]
188    fn test_rust_build_info_to_key_values_with_data() {
189        let info = RustBuildInfo {
190            rustc_version: Some("1.84.0"),
191            rust_channel: Some("stable"),
192            rustc_version_full: Some("rustc 1.84.0"),
193        };
194        let kvs = info.to_key_values();
195        assert_eq!(kvs.len(), 3);
196    }
197
198    #[test]
199    fn test_rust_build_info_partial_data() {
200        let info = RustBuildInfo {
201            rustc_version: Some("1.84.0"),
202            rust_channel: None,
203            rustc_version_full: None,
204        };
205        let kvs = info.to_key_values();
206        assert_eq!(kvs.len(), 1);
207    }
208}