saluki_env/workload/
entity.rs

1//! Entity identifiers.
2
3use std::{cmp::Ordering, fmt};
4
5use stringtheory::MetaString;
6use tracing::warn;
7
8const ENTITY_PREFIX_CONTAINER_ID: &str = "container_id://";
9const ENTITY_PREFIX_CONTAINER_IMAGE_METADATA: &str = "container_image_metadata://";
10const ENTITY_PREFIX_ECS_TASK: &str = "ecs_task://";
11const ENTITY_PREFIX_KUBERNETES_DEPLOYMENT: &str = "deployment://";
12const ENTITY_PREFIX_KUBERNETES_METADATA: &str = "kubernetes_metadata://";
13const ENTITY_PREFIX_KUBERNETES_NODE: &str = "kubernetes_node://";
14const ENTITY_PREFIX_POD_UID: &str = "kubernetes_pod_uid://";
15const ENTITY_PREFIX_PROCESS: &str = "process://";
16const ENTITY_PREFIX_CONTAINER_INODE: &str = "container_inode://";
17const ENTITY_PREFIX_CONTAINER_PID: &str = "container_pid://";
18
19const LOCAL_DATA_PREFIX_INODE: &str = "in-";
20const LOCAL_DATA_PREFIX_CID: &str = "ci-";
21const LOCAL_DATA_PREFIX_LEGACY_CID: &str = "cid-";
22
23/// An entity identifier.
24#[derive(Clone, Debug, Eq, Hash, PartialEq)]
25pub enum EntityId {
26    /// The global entity.
27    ///
28    /// Represents the root of the entity hierarchy, which is equivalent to a "global" scope. This is generally used
29    /// to represent a collection of metadata entries that aren't associated with any specific entity, but with
30    /// anything within the workload, such as host or cluster tags.
31    Global,
32
33    /// A container ID.
34    ///
35    /// This is generally a long hexadecimal string, as generally used by container runtimes like `containerd`.
36    Container(MetaString),
37
38    /// An OCI image manifest digest.
39    ContainerImageMetadata(MetaString),
40
41    /// An ECS task ARN.
42    EcsTask(MetaString),
43
44    /// A Kubernetes deployment, identified by `<namespace>/<name>`.
45    KubernetesDeployment(MetaString),
46
47    /// Kubernetes metadata, such as a namespace path.
48    KubernetesMetadata(MetaString),
49
50    /// A Kubernetes node name.
51    KubernetesNode(MetaString),
52
53    /// A Kubernetes pod UID.
54    ///
55    /// Represents the UUID of a specific Kubernetes pod.
56    PodUid(MetaString),
57
58    /// A process identifier reported by the remote tagger.
59    Process(MetaString),
60
61    /// A container inode.
62    ///
63    /// Represents the inode of the cgroups controller for a specific container.
64    ContainerInode(u64),
65
66    /// A container PID.
67    ///
68    /// Represents the PID of the process within a specific container.
69    ContainerPid(u32),
70}
71
72impl EntityId {
73    /// Creates an `EntityId` from Local Data.
74    ///
75    /// This method follows the same logic/behavior as the Datadog Agent's origin detection logic:
76    /// - If the input starts with `ci-`, we treat it as a container ID.
77    /// - If the input starts with `in-`, we treat it as a container cgroup controller inode.
78    /// - If the input contains a comma, we split the input and search for either a prefixed container ID or prefixed
79    ///   inode. If both are present, we use the container ID.
80    /// - If the input starts with `cid-`, we treat it as a container ID.
81    /// - If none of the above conditions are met, we assume the entire input is a container ID.
82    ///
83    /// If the input fails to be parsed in a valid fashion (for example, `in-` prefix but the remainder isn't a valid
84    /// integer), or is empty, `None` is returned.
85    pub fn from_local_data<S>(raw_local_data: S) -> Option<Self>
86    where
87        S: AsRef<str> + Into<MetaString>,
88    {
89        let local_data_value = raw_local_data.as_ref();
90        if local_data_value.is_empty() {
91            return None;
92        }
93
94        if local_data_value.contains(',') {
95            let mut maybe_container_inode = None;
96            for local_data_subvalue in local_data_value.split(',') {
97                match parse_local_data_value(local_data_subvalue) {
98                    // We always prefer the container ID if we get it.
99                    Ok(Some(Self::Container(cid))) => return Some(Self::Container(cid)),
100                    Ok(Some(Self::ContainerInode(inode))) => maybe_container_inode = Some(inode),
101                    Err(()) => {
102                        warn!(
103                            local_data = local_data_value,
104                            local_data_subvalue,
105                            "Failed parsing Local Data subvalue. Metric may be missing origin detection-based tags."
106                        );
107                    }
108                    _ => {}
109                }
110            }
111
112            // Return the container inode if we found one.
113            if let Some(inode) = maybe_container_inode {
114                return Some(Self::ContainerInode(inode));
115            }
116        }
117
118        // Try to parse the local data value as a single entity ID value, falling back to treating the entire value as a
119        // container ID otherwise.
120        match parse_local_data_value(local_data_value) {
121            // We always prefer the container ID if we get it.
122            Ok(Some(eid)) => Some(eid),
123            Ok(None) => Some(Self::Container(raw_local_data.into())),
124            Err(()) => {
125                warn!(
126                    local_data = local_data_value,
127                    "Failed parsing Local Data value. Metric may be missing origin detection-based tags."
128                );
129                None
130            }
131        }
132    }
133
134    /// Creates an `EntityId` from a Kubernetes pod UID.
135    ///
136    /// If the pod UID value is `"none"`, this will return `None`.
137    pub fn from_pod_uid<S>(pod_uid: S) -> Option<Self>
138    where
139        S: AsRef<str> + Into<MetaString>,
140    {
141        if pod_uid.as_ref() == "none" {
142            return None;
143        }
144        Some(Self::PodUid(pod_uid.into()))
145    }
146
147    /// Returns the inner container ID value, if this entity ID is a `Container`.
148    ///
149    /// Otherwise, `None` is returned and the original entity ID is consumed.
150    pub fn try_into_container(self) -> Option<MetaString> {
151        match self {
152            Self::Container(container_id) => Some(container_id),
153            _ => None,
154        }
155    }
156
157    fn precedence_value(&self) -> usize {
158        match self {
159            Self::Global => 0,
160            Self::Container(_) => 1,
161            Self::ContainerImageMetadata(_) => 2,
162            Self::EcsTask(_) => 3,
163            Self::KubernetesDeployment(_) => 4,
164            Self::KubernetesMetadata(_) => 5,
165            Self::KubernetesNode(_) => 6,
166            Self::PodUid(_) => 7,
167            Self::Process(_) => 8,
168            // These are local aliases used to resolve a container before the remote tagger entity lookup.
169            Self::ContainerInode(_) => 9,
170            Self::ContainerPid(_) => 10,
171        }
172    }
173}
174
175impl fmt::Display for EntityId {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            Self::Global => write!(f, "system://global"),
179            Self::Container(container_id) => write!(f, "{}{}", ENTITY_PREFIX_CONTAINER_ID, container_id),
180            Self::ContainerImageMetadata(digest) => write!(f, "{}{}", ENTITY_PREFIX_CONTAINER_IMAGE_METADATA, digest),
181            Self::EcsTask(task_arn) => write!(f, "{}{}", ENTITY_PREFIX_ECS_TASK, task_arn),
182            Self::KubernetesDeployment(deployment) => {
183                write!(f, "{}{}", ENTITY_PREFIX_KUBERNETES_DEPLOYMENT, deployment)
184            }
185            Self::KubernetesMetadata(metadata) => write!(f, "{}{}", ENTITY_PREFIX_KUBERNETES_METADATA, metadata),
186            Self::KubernetesNode(node) => write!(f, "{}{}", ENTITY_PREFIX_KUBERNETES_NODE, node),
187            Self::PodUid(pod_uid) => write!(f, "{}{}", ENTITY_PREFIX_POD_UID, pod_uid),
188            Self::Process(process_id) => write!(f, "{}{}", ENTITY_PREFIX_PROCESS, process_id),
189            Self::ContainerInode(inode) => write!(f, "{}{}", ENTITY_PREFIX_CONTAINER_INODE, inode),
190            Self::ContainerPid(pid) => write!(f, "{}{}", ENTITY_PREFIX_CONTAINER_PID, pid),
191        }
192    }
193}
194
195impl serde::Serialize for EntityId {
196    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
197    where
198        S: serde::Serializer,
199    {
200        // We have this manual implementation of `Serialize` just to avoid needing to bring in `serde_with` to get the
201        // helper that utilizes the `Display` implementation.
202        serializer.collect_str(self)
203    }
204}
205
206/// A wrapper for entity IDs that sorts them in a manner consistent with the expected precedence of entity IDs.
207///
208/// This type establishes a total ordering over entity IDs based on their logical precedence, which is as follows:
209///
210/// - global (highest precedence)
211/// - container
212/// - OCI image metadata
213/// - ECS task
214/// - Kubernetes deployment, metadata, node, and pod
215/// - process
216/// - local container inode and PID aliases (lowest precedence)
217///
218/// Wrapped entity IDs are be sorted highest to lowest precedence. For entity IDs with the same precedence, they're
219/// further ordered by their internal value. For entity IDs with a string identifier, lexicographical ordering is used.
220/// For entity IDs with a numeric identifier, numerical ordering is used.
221#[derive(Eq, PartialEq)]
222pub struct HighestPrecedenceEntityIdRef<'a>(&'a EntityId);
223
224impl<'a> From<&'a EntityId> for HighestPrecedenceEntityIdRef<'a> {
225    fn from(entity_id: &'a EntityId) -> Self {
226        Self(entity_id)
227    }
228}
229
230impl PartialOrd for HighestPrecedenceEntityIdRef<'_> {
231    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
232        Some(self.cmp(other))
233    }
234}
235
236impl Ord for HighestPrecedenceEntityIdRef<'_> {
237    fn cmp(&self, other: &Self) -> Ordering {
238        // Do the initial comparison based on the implicit precedence of each entity ID.
239        let self_precedence = self.0.precedence_value();
240        let other_precedence = other.0.precedence_value();
241        if self_precedence != other_precedence {
242            return self_precedence.cmp(&other_precedence);
243        }
244
245        // We have two entities at the same level of precedence, so we need to compare their actual values.
246        match (self.0, other.0) {
247            // Global entities are always equal.
248            (EntityId::Global, EntityId::Global) => Ordering::Equal,
249            (EntityId::Container(self_container_id), EntityId::Container(other_container_id)) => {
250                self_container_id.cmp(other_container_id)
251            }
252            (EntityId::ContainerImageMetadata(self_digest), EntityId::ContainerImageMetadata(other_digest)) => {
253                self_digest.cmp(other_digest)
254            }
255            (EntityId::EcsTask(self_task_arn), EntityId::EcsTask(other_task_arn)) => self_task_arn.cmp(other_task_arn),
256            (EntityId::KubernetesDeployment(self_deployment), EntityId::KubernetesDeployment(other_deployment)) => {
257                self_deployment.cmp(other_deployment)
258            }
259            (EntityId::KubernetesMetadata(self_metadata), EntityId::KubernetesMetadata(other_metadata)) => {
260                self_metadata.cmp(other_metadata)
261            }
262            (EntityId::KubernetesNode(self_node), EntityId::KubernetesNode(other_node)) => self_node.cmp(other_node),
263            (EntityId::PodUid(self_pod_uid), EntityId::PodUid(other_pod_uid)) => self_pod_uid.cmp(other_pod_uid),
264            (EntityId::Process(self_process_id), EntityId::Process(other_process_id)) => {
265                self_process_id.cmp(other_process_id)
266            }
267            (EntityId::ContainerInode(self_inode), EntityId::ContainerInode(other_inode)) => {
268                self_inode.cmp(other_inode)
269            }
270            (EntityId::ContainerPid(self_pid), EntityId::ContainerPid(other_pid)) => self_pid.cmp(other_pid),
271            _ => unreachable!("entities with different precedence should not be compared"),
272        }
273    }
274}
275
276fn parse_local_data_value(raw_local_data_value: &str) -> Result<Option<EntityId>, ()> {
277    if raw_local_data_value.starts_with(LOCAL_DATA_PREFIX_CID) {
278        let cid = raw_local_data_value.trim_start_matches(LOCAL_DATA_PREFIX_CID);
279        Ok(Some(EntityId::Container(cid.into())))
280    } else if raw_local_data_value.starts_with(LOCAL_DATA_PREFIX_INODE) {
281        let inode = raw_local_data_value
282            .trim_start_matches(LOCAL_DATA_PREFIX_INODE)
283            .parse()
284            .map_err(|_| ())?;
285        Ok(Some(EntityId::ContainerInode(inode)))
286    } else if raw_local_data_value.starts_with(LOCAL_DATA_PREFIX_LEGACY_CID) {
287        let cid = raw_local_data_value.trim_start_matches(LOCAL_DATA_PREFIX_LEGACY_CID);
288        Ok(Some(EntityId::Container(cid.into())))
289    } else {
290        Ok(None)
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn local_data() {
300        const PREFIX_CID: &str = "ci-singlecontainerid";
301        const PREFIX_LEGACY_CID: &str = "cid-singlecontainerid";
302        const CID: EntityId = EntityId::Container(MetaString::from_static("singlecontainerid"));
303        const PREFIX_INODE: &str = "in-12345";
304        const INODE: EntityId = EntityId::ContainerInode(12345);
305
306        let cases = [
307            // Empty inputs aren't valid.
308            ("".into(), None),
309            // Invalid container inode values.
310            ("in-notanumber".into(), None),
311            // Fallback to treat any unparsed value as container ID.
312            ("random".into(), Some(EntityId::Container("random".into()))),
313            // Single prefixed values.
314            (PREFIX_CID.into(), Some(CID.clone())),
315            (PREFIX_INODE.into(), Some(INODE.clone())),
316            (PREFIX_LEGACY_CID.into(), Some(CID.clone())),
317            // Multiple prefixed values, comma separated.
318            //
319            // We should always prefer container ID over inode. We also test invalid values here since we should
320            // ignore them as we iterate over the split values.
321            (format!("{},{}", PREFIX_CID, PREFIX_INODE), Some(CID.clone())),
322            (format!("{},{}", PREFIX_INODE, PREFIX_CID), Some(CID.clone())),
323            (format!("{},{}", PREFIX_LEGACY_CID, PREFIX_INODE), Some(CID.clone())),
324            (format!("{},{}", PREFIX_INODE, PREFIX_LEGACY_CID), Some(CID.clone())),
325            (format!("{},invalid", PREFIX_CID), Some(CID.clone())),
326            (format!("{},invalid", PREFIX_LEGACY_CID), Some(CID.clone())),
327            (format!("{},invalid", PREFIX_INODE), Some(INODE.clone())),
328        ];
329
330        for (input, expected) in cases {
331            let actual = EntityId::from_local_data(input);
332            assert_eq!(actual, expected);
333        }
334    }
335
336    #[test]
337    fn pod_uid_valid() {
338        let pod_uid = "abcdef1234567890";
339        let entity_id = EntityId::from_pod_uid(pod_uid).unwrap();
340        assert_eq!(entity_id, EntityId::PodUid(MetaString::from(pod_uid)));
341    }
342
343    #[test]
344    fn pod_uid_none() {
345        let pod_uid = "none";
346        let entity_id = EntityId::from_pod_uid(pod_uid);
347        assert!(entity_id.is_none());
348    }
349}