Skip to main content

kerniq_telemetry/
resource.rs

1//! Service identity attached to every signal as OpenTelemetry resource
2//! attributes.
3
4use opentelemetry::KeyValue;
5
6/// `service.name`.
7pub const SERVICE_NAME: &str = "service.name";
8/// `service.version`.
9pub const SERVICE_VERSION: &str = "service.version";
10/// `service.namespace`.
11pub const SERVICE_NAMESPACE: &str = "service.namespace";
12/// `service.instance.id`.
13pub const SERVICE_INSTANCE_ID: &str = "service.instance.id";
14/// `deployment.environment.name`.
15pub const DEPLOYMENT_ENVIRONMENT: &str = "deployment.environment.name";
16
17/// Who is emitting telemetry. Converted into an OpenTelemetry `Resource` by
18/// `kerniq-observability`.
19///
20/// Unset fields fall back to the standard environment variables
21/// (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`) through the SDK's
22/// resource detectors.
23#[derive(Debug, Clone, Default)]
24pub struct ServiceInfo {
25    /// `service.name`.
26    pub name: Option<String>,
27    /// `service.version`.
28    pub version: Option<String>,
29    /// `service.namespace`.
30    pub namespace: Option<String>,
31    /// `deployment.environment.name` (`production`, `staging`, ...).
32    pub environment: Option<String>,
33    /// `service.instance.id`. Unique per process; Kerniq generates one when
34    /// not set.
35    pub instance_id: Option<String>,
36    /// Additional resource attributes (region, cluster, team ...).
37    pub attributes: Vec<KeyValue>,
38}
39
40impl ServiceInfo {
41    /// All configured attributes, `service.name` excluded (the SDK resource
42    /// builder handles it separately so it can apply its fallback).
43    pub fn to_attributes(&self) -> Vec<KeyValue> {
44        let mut out = Vec::with_capacity(4 + self.attributes.len());
45        if let Some(v) = &self.version {
46            out.push(KeyValue::new(SERVICE_VERSION, v.clone()));
47        }
48        if let Some(v) = &self.namespace {
49            out.push(KeyValue::new(SERVICE_NAMESPACE, v.clone()));
50        }
51        if let Some(v) = &self.environment {
52            out.push(KeyValue::new(DEPLOYMENT_ENVIRONMENT, v.clone()));
53        }
54        if let Some(v) = &self.instance_id {
55            out.push(KeyValue::new(SERVICE_INSTANCE_ID, v.clone()));
56        }
57        out.extend(self.attributes.iter().cloned());
58        out
59    }
60}
61
62/// A process-unique instance id: `<hostname>-<pid>-<start time>`, without
63/// requiring a UUID dependency. Stable for the life of the process.
64pub fn default_instance_id() -> String {
65    let host = std::env::var("HOSTNAME")
66        .or_else(|_| std::env::var("COMPUTERNAME"))
67        .unwrap_or_else(|_| "localhost".to_owned());
68    let started = std::time::SystemTime::now()
69        .duration_since(std::time::UNIX_EPOCH)
70        .map(|d| d.as_millis())
71        .unwrap_or_default();
72    format!("{host}-{}-{started:x}", std::process::id())
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn attributes() {
81        let info = ServiceInfo {
82            name: Some("payment-api".into()),
83            version: Some("1.0.0".into()),
84            environment: Some("production".into()),
85            attributes: vec![KeyValue::new("cloud.region", "eu-west-1")],
86            ..ServiceInfo::default()
87        };
88        let attrs = info.to_attributes();
89        let keys: Vec<_> = attrs.iter().map(|kv| kv.key.as_str()).collect();
90        assert_eq!(
91            keys,
92            [SERVICE_VERSION, DEPLOYMENT_ENVIRONMENT, "cloud.region"]
93        );
94    }
95
96    #[test]
97    fn instance_id_contains_pid() {
98        assert!(default_instance_id().contains(&std::process::id().to_string()));
99    }
100}