async_profiler_agent/metadata/
aws.rs1use reqwest::Method;
9use serde::Deserialize;
10use thiserror::Error;
11
12use crate::metadata::OrderedF64;
13
14use super::AgentMetadata;
15
16#[derive(Error, Debug)]
19pub enum FargateMetadataToAgentMetadataError {
20 #[error("unable to parse task ARN as a valid ARN")]
22 TaskArnInvalid(#[from] aws_arn::Error),
23 #[error("AWS account id not found in Fargate metadata")]
25 AccountIdNotFound,
26 #[error("AWS region not found in Fargate metadata")]
28 AwsRegionNotFound,
29}
30
31#[derive(Error, Debug)]
33#[error("profiler metadata error: {0}")]
34#[allow(clippy::large_enum_variant)]
37pub enum AwsProfilerMetadataError {
38 #[error("failed to create profiler metadata file: {0}")]
40 FailedToCreateFile(#[from] std::io::Error),
41
42 #[error("failed fetching valid Fargate metadata: {0}")]
44 FargateMetadataToAgentMetadataError(#[from] FargateMetadataToAgentMetadataError),
45
46 #[error("retrieved invalid endpoint URI from ECS_CONTAINER_METADATA_URI_V4: {0}")]
48 InvalidUri(String),
49
50 #[error("failed to fetch metadata from endpoint over HTTP: {0}")]
52 FailedToFetchMetadataFromEndpoint(reqwest::Error),
53
54 #[error("failed to fetch metadata from IMDS endpoint over HTTP: {0}")]
56 FailedToFetchMetadataFromImds(#[from] aws_config::imds::client::error::ImdsError),
57
58 #[error("failed to parse metadata as valid UTF-8 from endpoint over HTTP: {0}")]
61 FailedToParseMetadataFromEndpoint(reqwest::Error),
62
63 #[error("failed to serialize metadata file: {0}")]
66 FailedToSerializeMetadataFile(#[from] serde_json::Error),
67}
68
69#[derive(Debug, Deserialize, PartialEq, Eq)]
70#[serde(rename_all = "camelCase")]
71struct ImdsEc2InstanceMetadata {
72 account_id: String,
73 region: String,
74 #[allow(dead_code)]
75 instance_type: String,
76 instance_id: String,
77}
78
79async fn read_ec2_metadata() -> Result<ImdsEc2InstanceMetadata, AwsProfilerMetadataError> {
80 let imds = aws_config::imds::Client::builder().build();
81 let imds_document = imds
82 .get("/latest/dynamic/instance-identity/document")
83 .await?;
84
85 Ok(serde_json::from_str(imds_document.as_ref())?)
86}
87
88#[derive(Deserialize, Debug, PartialEq, Eq)]
89struct FargateLimits {
90 #[serde(rename = "CPU")]
91 cpu: Option<OrderedF64>,
92 #[serde(rename = "Memory")]
93 memory: Option<u64>,
94}
95
96#[derive(Deserialize, Debug, PartialEq, Eq)]
97struct FargateMetadata {
98 #[serde(rename = "Cluster")]
99 cluster: String,
100 #[serde(rename = "TaskARN")]
101 task_arn: String,
102 #[serde(rename = "Limits")]
106 limits: Option<FargateLimits>,
107}
108
109async fn read_fargate_metadata(
110 http_client: &reqwest::Client,
111) -> Result<FargateMetadata, AwsProfilerMetadataError> {
112 let Ok(md_uri) = std::env::var("ECS_CONTAINER_METADATA_URI_V4") else {
113 return Err(AwsProfilerMetadataError::InvalidUri(
114 "not running on fargate".into(),
115 ));
116 };
117 let uri = format!("{md_uri}/task",);
120
121 let req = http_client
122 .request(Method::GET, uri.clone())
123 .build()
124 .map_err(|_e| AwsProfilerMetadataError::InvalidUri(uri))?;
126 let res = http_client
127 .execute(req)
128 .await
129 .map_err(AwsProfilerMetadataError::FailedToFetchMetadataFromEndpoint)?;
130 let body_str = res
131 .text()
132 .await
133 .map_err(AwsProfilerMetadataError::FailedToParseMetadataFromEndpoint)?;
134
135 Ok(serde_json::from_str(&body_str)?)
136}
137
138impl super::AgentMetadata {
139 fn from_imds_ec2_instance_metadata(
140 imds_ec2_instance_metadata: ImdsEc2InstanceMetadata,
141 ) -> Self {
142 Self::Ec2AgentMetadata {
143 aws_account_id: imds_ec2_instance_metadata.account_id,
144 aws_region_id: imds_ec2_instance_metadata.region,
145 ec2_instance_id: imds_ec2_instance_metadata.instance_id,
146 ec2_instance_type: imds_ec2_instance_metadata.instance_type,
147 }
148 }
149
150 fn try_from_fargate_metadata(
151 fargate_metadata: FargateMetadata,
152 ) -> Result<Self, FargateMetadataToAgentMetadataError> {
153 let ecs_task_arn: aws_arn::ResourceName = fargate_metadata.task_arn.parse()?;
154
155 Ok(Self::FargateAgentMetadata {
156 aws_account_id: ecs_task_arn
157 .account_id
158 .ok_or(FargateMetadataToAgentMetadataError::AccountIdNotFound)?
159 .to_string(),
160 aws_region_id: ecs_task_arn
161 .region
162 .ok_or(FargateMetadataToAgentMetadataError::AwsRegionNotFound)?
163 .to_string(),
164 ecs_task_arn: fargate_metadata.task_arn,
165 ecs_cluster_arn: fargate_metadata.cluster,
166 cpu_limit: fargate_metadata
167 .limits
168 .as_ref()
169 .and_then(|limits| limits.cpu),
170 memory_limit: fargate_metadata
171 .limits
172 .as_ref()
173 .and_then(|limits| limits.memory),
174 })
175 }
176}
177
178pub async fn load_agent_metadata() -> Result<AgentMetadata, AwsProfilerMetadataError> {
186 let agent_metadata: AgentMetadata = match read_ec2_metadata().await {
187 Ok(imds_ec2_instance_metadata) => {
188 AgentMetadata::from_imds_ec2_instance_metadata(imds_ec2_instance_metadata)
189 }
190 Err(_) => {
191 let http_client = reqwest::Client::new();
192 let fargate_metadata = read_fargate_metadata(&http_client).await?;
193
194 AgentMetadata::try_from_fargate_metadata(fargate_metadata)?
195 }
196 };
197 Ok(agent_metadata)
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use test_case::test_case;
204
205 #[test]
222 fn test_imds_ec2_metadata() {
223 let json_str = r#"
224{
225 "accountId" : "123456789012",
226 "architecture" : "x86_64",
227 "availabilityZone" : "eu-west-1b",
228 "billingProducts" : null,
229 "devpayProductCodes" : null,
230 "marketplaceProductCodes" : null,
231 "imageId" : "ami-052d4b310b0a459ff",
232 "instanceId" : "i-092eba08c089f6325",
233 "instanceType" : "c5.4xlarge",
234 "kernelId" : null,
235 "pendingTime" : "2025-03-20T16:41:24Z",
236 "privateIp" : "10.65.149.216",
237 "ramdiskId" : null,
238 "region" : "eu-west-1",
239 "version" : "2017-09-30"
240}"#;
241
242 let imds_ec2_instance_metadata: ImdsEc2InstanceMetadata =
243 serde_json::from_str(&json_str).unwrap();
244
245 assert_eq!(
246 imds_ec2_instance_metadata,
247 ImdsEc2InstanceMetadata {
248 account_id: "123456789012".to_owned(),
249 region: "eu-west-1".to_owned(),
250 instance_type: "c5.4xlarge".to_owned(),
251 instance_id: "i-092eba08c089f6325".to_owned(),
252 }
253 );
254
255 let agent_metadata =
256 AgentMetadata::from_imds_ec2_instance_metadata(imds_ec2_instance_metadata);
257
258 let expected = AgentMetadata::ec2_agent_metadata(
259 "123456789012".to_owned(),
260 "eu-west-1".to_owned(),
261 "i-092eba08c089f6325".to_owned(),
262 )
263 .with_ec2_instance_type("c5.4xlarge".to_owned())
264 .build();
265
266 assert_eq!(agent_metadata, expected);
267 }
268
269 #[test_case(
270 r#"{
271 "Cluster": "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster",
272 "TaskARN": "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f"
273}"#,
274 None,
275 None,
276 None
277 ; "no_limits"
278 )]
279 #[test_case(
280 r#"{
281 "Cluster": "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster",
282 "TaskARN": "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f",
283 "Limits": {}
284}"#,
285 Some(FargateLimits { cpu: None, memory: None }),
286 None,
287 None
288 ; "empty_limits"
289 )]
290 #[test_case(
291 r#"{
292 "Cluster": "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster",
293 "TaskARN": "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f",
294 "Family": "profiler-metadata",
295 "Revision": "1",
296 "DesiredStatus": "RUNNING",
297 "KnownStatus": "NONE",
298 "Limits": {
299 "CPU": 0.25,
300 "Memory": 2048
301 },
302 "PullStartedAt": "2025-03-20T16:41:24.713942268Z",
303 "PullStoppedAt": "2025-03-20T16:41:25.623883595Z",
304 "AvailabilityZone": "us-east-1f",
305 "LaunchType": "FARGATE",
306 "Containers": [
307 {
308 "DockerId": "5261e761e0e2a3d92da3f02c8e5bab1f-3356750833",
309 "Name": "profiler-metadata",
310 "DockerName": "profiler-metadata",
311 "Image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/profiler-metadata",
312 "ImageID": "sha256:ad9d89a36c31afef34c79e05263b06087ad354796cfd90c66ced30f40ea2dbf4",
313 "Labels": {
314 "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster",
315 "com.amazonaws.ecs.container-name": "profiler-metadata",
316 "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f",
317 "com.amazonaws.ecs.task-definition-family": "profiler-metadata",
318 "com.amazonaws.ecs.task-definition-version": "1"
319 },
320 "DesiredStatus": "RUNNING",
321 "KnownStatus": "PULLED",
322 "Limits": {
323 "CPU": 0
324 },
325 "Type": "NORMAL",
326 "LogDriver": "awslogs",
327 "LogOptions": {
328 "awslogs-create-group": "true",
329 "awslogs-group": "/ecs/profiler-metadata",
330 "awslogs-region": "us-east-1",
331 "awslogs-stream": "ecs/profiler-metadata/5261e761e0e2a3d92da3f02c8e5bab1f",
332 "max-buffer-size": "25m",
333 "mode": "non-blocking"
334 },
335 "ContainerARN": "arn:aws:ecs:us-east-1:123456789012:container/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f/f4094744-1b40-4701-9f26-ad84ebb709d7",
336 "Networks": [
337 {
338 "NetworkMode": "awsvpc",
339 "IPv4Addresses": [
340 "172.31.233.169"
341 ],
342 "AttachmentIndex": 0,
343 "MACAddress": "16:ff:d6:e1:dc:99",
344 "IPv4SubnetCIDRBlock": "172.31.192.0/20",
345 "DomainNameServers": [
346 "172.31.0.2"
347 ],
348 "DomainNameSearchList": [
349 "ec2.internal"
350 ],
351 "PrivateDNSName": "ip-172-31-233-169.ec2.internal",
352 "SubnetGatewayIpv4Address": "172.31.192.1/20"
353 }
354 ],
355 "Snapshotter": "overlayfs"
356 }
357 ],
358 "ClockDrift": {
359 "ClockErrorBound": 0.3148955,
360 "ReferenceTimestamp": "2025-03-20T16:41:24Z",
361 "ClockSynchronizationStatus": "SYNCHRONIZED"
362 },
363 "EphemeralStorageMetrics": {
364 "Utilized": 208,
365 "Reserved": 20496
366 }
367}"#,
368 Some(FargateLimits { cpu: Some(0.25.into()), memory: Some(2048) }),
369 Some(0.25.into()),
370 Some(2048)
371 ; "with_limits"
372 )]
373 fn test_fargate_metadata(
374 json_str: &str,
375 expected_limits: Option<FargateLimits>,
376 _expected_cpu_limit: Option<OrderedF64>,
377 _expected_memory_limit: Option<u64>,
378 ) {
379 let fargate_metadata: FargateMetadata = serde_json::from_str(json_str).unwrap();
380
381 assert_eq!(
382 fargate_metadata,
383 FargateMetadata {
384 cluster: "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster"
385 .to_owned(),
386 task_arn: "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f".to_owned(),
387 limits: expected_limits,
388 }
389 );
390
391 let agent_metadata = AgentMetadata::try_from_fargate_metadata(fargate_metadata).unwrap();
392
393 assert_eq!(
394 agent_metadata,
395 AgentMetadata::FargateAgentMetadata {
396 aws_account_id: "123456789012".to_owned(),
397 aws_region_id: "us-east-1".to_owned(),
398 ecs_task_arn: "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f".to_owned(),
399 ecs_cluster_arn: "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster".to_owned(),
400 cpu_limit: _expected_cpu_limit,
401 memory_limit: _expected_memory_limit,
402 }
403 )
404 }
405}