saluki_env/workload/
entity.rs

1use std::{cmp::Ordering, fmt};
2
3use stringtheory::MetaString;
4
5const ENTITY_PREFIX_POD_UID: &str = "kubernetes_pod_uid://";
6const ENTITY_PREFIX_CONTAINER_ID: &str = "container_id://";
7const ENTITY_PREFIX_CONTAINER_INODE: &str = "container_inode://";
8const ENTITY_PREFIX_CONTAINER_PID: &str = "container_pid://";
9
10const RAW_CONTAINER_ID_PREFIX_INODE: &str = "in-";
11const RAW_CONTAINER_ID_PREFIX_CID: &str = "ci-";
12
13/// An entity identifier.
14#[derive(Clone, Debug, Eq, Hash, PartialEq)]
15pub enum EntityId {
16    /// The global entity.
17    ///
18    /// Represents the root of the entity hierarchy, which is equivalent to a "global" scope. This is generally used
19    /// to represent a collection of metadata entries that are not associated with any specific entity, but with
20    /// anything within the workload, such as host or cluster tags.
21    Global,
22
23    /// A Kubernetes pod UID.
24    ///
25    /// Represents the UUID of a specific Kubernetes pod.
26    PodUid(MetaString),
27
28    /// A container ID.
29    ///
30    /// This is generally a long hexadecimal string, as generally used by container runtimes like `containerd`.
31    Container(MetaString),
32
33    /// A container inode.
34    ///
35    /// Represents the inode of the cgroups controller for a specific container.
36    ContainerInode(u64),
37
38    /// A container PID.
39    ///
40    /// Represents the PID of the process within a specific container.
41    ContainerPid(u32),
42}
43
44impl EntityId {
45    /// Creates an `EntityId` from a raw container ID.
46    ///
47    /// This method handles two special cases when the raw container ID is prefixed with "ci-" or "in-":
48    ///
49    /// - "ci-" indicates that the raw container ID is a real container ID, but just with an identifying prefix. The
50    ///   prefix is stripped and the remainder is treated as the container ID.
51    /// - "in-" indicates that the raw container ID is actually the inode of the cgroups controller for a container. The
52    ///   prefix is stripped and the remainder is parsed as an integer, and the result is treated as the container inode.
53    ///
54    /// If the raw container ID does not start with either of these prefixes, we assume the entire value is the
55    /// container ID. If the raw container ID starts with the "in-" prefix, but the remainder is not a valid integer,
56    /// `None` is returned.
57    pub fn from_raw_container_id<S>(raw_container_id: S) -> Option<Self>
58    where
59        S: AsRef<str> + Into<MetaString>,
60    {
61        if raw_container_id.as_ref().starts_with(RAW_CONTAINER_ID_PREFIX_INODE) {
62            // We have a "container ID" that is actually the inode of the cgroups controller for the container where
63            // the metric originated. We treat this separately from true container IDs, which are typically 64 character
64            // hexadecimal strings.
65            let raw_inode = raw_container_id
66                .as_ref()
67                .trim_start_matches(RAW_CONTAINER_ID_PREFIX_INODE);
68            let inode = raw_inode.parse().ok()?;
69            Some(Self::ContainerInode(inode))
70        } else if raw_container_id.as_ref().starts_with(RAW_CONTAINER_ID_PREFIX_CID) {
71            // We have a real container ID, but just with an identifying prefix. We can simply strip the prefix and
72            // treat the remainder as the container ID.
73            let raw_cid = raw_container_id
74                .as_ref()
75                .trim_start_matches(RAW_CONTAINER_ID_PREFIX_CID);
76            Some(Self::Container(raw_cid.into()))
77        } else {
78            Some(Self::Container(raw_container_id.into()))
79        }
80    }
81
82    /// Creates an `EntityId` from a Kubernetes pod UID.
83    ///
84    /// If the pod UID value is "none", this will return `None`.
85    pub fn from_pod_uid<S>(pod_uid: S) -> Option<Self>
86    where
87        S: AsRef<str> + Into<MetaString>,
88    {
89        if pod_uid.as_ref() == "none" {
90            return None;
91        }
92        Some(Self::PodUid(pod_uid.into()))
93    }
94
95    /// Returns the inner container ID value, if this entity ID is a `Container`.
96    ///
97    /// Otherwise, `None` is returned and the original entity ID is consumed.
98    pub fn try_into_container(self) -> Option<MetaString> {
99        match self {
100            Self::Container(container_id) => Some(container_id),
101            _ => None,
102        }
103    }
104
105    fn precedence_value(&self) -> usize {
106        match self {
107            Self::Global => 0,
108            Self::PodUid(_) => 1,
109            Self::Container(_) => 2,
110            Self::ContainerInode(_) => 3,
111            Self::ContainerPid(_) => 4,
112        }
113    }
114}
115
116impl fmt::Display for EntityId {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            Self::Global => write!(f, "system://global"),
120            Self::PodUid(pod_uid) => write!(f, "{}{}", ENTITY_PREFIX_POD_UID, pod_uid),
121            Self::Container(container_id) => write!(f, "{}{}", ENTITY_PREFIX_CONTAINER_ID, container_id),
122            Self::ContainerInode(inode) => write!(f, "{}{}", ENTITY_PREFIX_CONTAINER_INODE, inode),
123            Self::ContainerPid(pid) => write!(f, "{}{}", ENTITY_PREFIX_CONTAINER_PID, pid),
124        }
125    }
126}
127
128impl serde::Serialize for EntityId {
129    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
130    where
131        S: serde::Serializer,
132    {
133        // We have this manual implementation of `Serialize` just to avoid needing to bring in `serde_with` to get the
134        // helper that utilizes the `Display` implementation.
135        serializer.collect_str(self)
136    }
137}
138
139/// A wrapper for entity IDs that sorts them in a manner consistent with the expected precedence of entity IDs.
140///
141/// This type establishes a total ordering over entity IDs based on their logical precedence, which is as follows:
142///
143/// - global (highest precedence)
144/// - pod
145/// - container
146/// - container inode
147/// - container PID (lowest precedence)
148///
149/// Wrapped entity IDs are be sorted highest to lowest precedence. For entity IDs with the same precedence, they are
150/// further ordered by their internal value. For entity IDs with a string identifier, lexicographical ordering is used.
151/// For entity IDs with a numeric identifier, numerical ordering is used.
152#[derive(Eq, PartialEq)]
153pub struct HighestPrecedenceEntityIdRef<'a>(&'a EntityId);
154
155impl<'a> From<&'a EntityId> for HighestPrecedenceEntityIdRef<'a> {
156    fn from(entity_id: &'a EntityId) -> Self {
157        Self(entity_id)
158    }
159}
160
161impl PartialOrd for HighestPrecedenceEntityIdRef<'_> {
162    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
163        Some(self.cmp(other))
164    }
165}
166
167impl Ord for HighestPrecedenceEntityIdRef<'_> {
168    fn cmp(&self, other: &Self) -> Ordering {
169        // Do the initial comparison based on the implicit precedence of each entity ID.
170        let self_precedence = self.0.precedence_value();
171        let other_precedence = other.0.precedence_value();
172        if self_precedence != other_precedence {
173            return self_precedence.cmp(&other_precedence);
174        }
175
176        // We have two entities at the same level of precedence, so we need to compare their actual values.
177        match (self.0, other.0) {
178            // Global entities are always equal.
179            (EntityId::Global, EntityId::Global) => Ordering::Equal,
180            (EntityId::PodUid(self_pod_uid), EntityId::PodUid(other_pod_uid)) => self_pod_uid.cmp(other_pod_uid),
181            (EntityId::Container(self_container_id), EntityId::Container(other_container_id)) => {
182                self_container_id.cmp(other_container_id)
183            }
184            (EntityId::ContainerInode(self_inode), EntityId::ContainerInode(other_inode)) => {
185                self_inode.cmp(other_inode)
186            }
187            (EntityId::ContainerPid(self_pid), EntityId::ContainerPid(other_pid)) => self_pid.cmp(other_pid),
188            _ => unreachable!("entities with different precedence should not be compared"),
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn raw_container_id_inode_valid() {
199        let container_inode = 123456;
200        let raw_container_id = format!("{}{}", RAW_CONTAINER_ID_PREFIX_INODE, container_inode);
201        let entity_id = EntityId::from_raw_container_id(raw_container_id).unwrap();
202        assert_eq!(entity_id, EntityId::ContainerInode(container_inode));
203    }
204
205    #[test]
206    fn raw_container_id_inode_invalid() {
207        let raw_container_id = format!("{}invalid", RAW_CONTAINER_ID_PREFIX_INODE);
208        let entity_id = EntityId::from_raw_container_id(raw_container_id);
209        assert!(entity_id.is_none());
210    }
211
212    #[test]
213    fn raw_container_id_cid() {
214        let container_id = "abcdef1234567890";
215        let raw_container_id = format!("{}{}", RAW_CONTAINER_ID_PREFIX_CID, container_id);
216        let entity_id = EntityId::from_raw_container_id(raw_container_id).unwrap();
217        assert_eq!(entity_id, EntityId::Container(MetaString::from(container_id)));
218    }
219
220    #[test]
221    fn pod_uid_valid() {
222        let pod_uid = "abcdef1234567890";
223        let entity_id = EntityId::from_pod_uid(pod_uid).unwrap();
224        assert_eq!(entity_id, EntityId::PodUid(MetaString::from(pod_uid)));
225    }
226
227    #[test]
228    fn pod_uid_none() {
229        let pod_uid = "none";
230        let entity_id = EntityId::from_pod_uid(pod_uid);
231        assert!(entity_id.is_none());
232    }
233}