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