Skip to content

Commit b5ef538

Browse files
felixscheinostMichele Mancioppi
and
Michele Mancioppi
authored
Improve detection of resource attributes on ECS (#4574)
* Improve detection of resource attributes on ECS This improves the detection of resource attributes on ECS by fetching ECS metadata from `ECS_CONTAINER_METADATA_URI` or `ECS_CONTAINER_METADATA_URI_V4`. Previously only `CONTAINER_NAME` and `CONTAINER_ID` id were set. Now we set: - CONTAINER_ID - CONTAINER_NAME - AWS_ECS_CONTAINER_ARN - CONTAINER_IMAGE_NAME - CONTAINER_IMAGE_TAG - aws.ecs.container.image.id - AWS_LOG_GROUP_ARNS - AWS_LOG_GROUP_NAMES - AWS_LOG_STREAM_NAMES - AWS_ECS_TASK_ARN - AWS_ECS_TASK_FAMILY - AWS_ECS_TASK_REVISION Especially AWS_LOG_GROUP_ARNS is important so that connection of traces to logs works OOTB on X-Ray. * Change „24“ to „26“ in fixtures for consistency `CONTAINER_NAME` and `AWS_ECS_TASK_REVISION` should match * Implement a few more attributes, fix ARNs This commit adds implementations for the `aws.ecs.launchtype` and `aws.logs.stream.arns` attributes, as well as fixing the generation of log group ARNs. * EcsResource: Record log group without trailing :* Both with and without trailing `:*` are valid formats but there is a bug in the OpenTelementry collector which can’t handle the trailing `:*` (for now) (see open-telemetry/opentelemetry-collector-contrib#13702) So remove addition of the trailing `:*` for now. Co-authored-by: Michele Mancioppi <[email protected]>
1 parent 66285e2 commit b5ef538

File tree

6 files changed

+543
-42
lines changed

6 files changed

+543
-42
lines changed

sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResource.java

+214-21
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,30 @@
55

66
package io.opentelemetry.sdk.extension.aws.resource;
77

8+
import com.fasterxml.jackson.core.JsonFactory;
9+
import com.fasterxml.jackson.core.JsonParser;
10+
import com.fasterxml.jackson.core.JsonToken;
811
import io.opentelemetry.api.common.Attributes;
912
import io.opentelemetry.api.common.AttributesBuilder;
1013
import io.opentelemetry.sdk.resources.Resource;
1114
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
12-
import java.net.InetAddress;
13-
import java.net.UnknownHostException;
15+
import java.io.IOException;
16+
import java.util.Collections;
1417
import java.util.Map;
18+
import java.util.Optional;
1519
import java.util.logging.Level;
1620
import java.util.logging.Logger;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
import javax.annotation.Nullable;
1724

1825
/**
1926
* A factory for a {@link Resource} which provides information about the current ECS container if
2027
* running on AWS ECS.
2128
*/
2229
public final class EcsResource {
2330
private static final Logger logger = Logger.getLogger(EcsResource.class.getName());
24-
31+
private static final JsonFactory JSON_FACTORY = new JsonFactory();
2532
private static final String ECS_METADATA_KEY_V4 = "ECS_CONTAINER_METADATA_URI_V4";
2633
private static final String ECS_METADATA_KEY_V3 = "ECS_CONTAINER_METADATA_URI";
2734

@@ -36,38 +43,224 @@ public static Resource get() {
3643
}
3744

3845
private static Resource buildResource() {
39-
return buildResource(System.getenv(), new DockerHelper());
46+
return buildResource(System.getenv(), new SimpleHttpClient());
4047
}
4148

4249
// Visible for testing
43-
static Resource buildResource(Map<String, String> sysEnv, DockerHelper dockerHelper) {
44-
if (!isOnEcs(sysEnv)) {
45-
return Resource.empty();
50+
static Resource buildResource(Map<String, String> sysEnv, SimpleHttpClient httpClient) {
51+
// Note: If V4 is set V3 is set as well, so check V4 first.
52+
String ecsMetadataUrl =
53+
sysEnv.getOrDefault(ECS_METADATA_KEY_V4, sysEnv.getOrDefault(ECS_METADATA_KEY_V3, ""));
54+
if (!ecsMetadataUrl.isEmpty()) {
55+
AttributesBuilder attrBuilders = Attributes.builder();
56+
fetchMetadata(httpClient, ecsMetadataUrl, attrBuilders);
57+
// For TaskARN, Family, Revision.
58+
// May put the same attribute twice but that shouldn't matter.
59+
fetchMetadata(httpClient, ecsMetadataUrl + "/task", attrBuilders);
60+
return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL);
4661
}
62+
// Not running on ECS
63+
return Resource.empty();
64+
}
4765

48-
AttributesBuilder attrBuilders = Attributes.builder();
66+
static void fetchMetadata(
67+
SimpleHttpClient httpClient, String url, AttributesBuilder attrBuilders) {
68+
String json = httpClient.fetchString("GET", url, Collections.emptyMap(), null);
69+
if (json.isEmpty()) {
70+
return;
71+
}
4972
attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS);
5073
attrBuilders.put(
5174
ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.AWS_ECS);
52-
try {
53-
String hostName = InetAddress.getLocalHost().getHostName();
54-
attrBuilders.put(ResourceAttributes.CONTAINER_NAME, hostName);
55-
} catch (UnknownHostException e) {
56-
logger.log(Level.WARNING, "Could not get docker container name from hostname.", e);
75+
try (JsonParser parser = JSON_FACTORY.createParser(json)) {
76+
parser.nextToken();
77+
LogArnBuilder logArnBuilder = new LogArnBuilder();
78+
parseResponse(parser, attrBuilders, logArnBuilder);
79+
80+
logArnBuilder
81+
.getLogGroupArn()
82+
.ifPresent(
83+
logGroupArn -> {
84+
attrBuilders.put(
85+
ResourceAttributes.AWS_LOG_GROUP_ARNS, Collections.singletonList(logGroupArn));
86+
});
87+
88+
logArnBuilder
89+
.getLogStreamArn()
90+
.ifPresent(
91+
logStreamArn -> {
92+
attrBuilders.put(
93+
ResourceAttributes.AWS_LOG_STREAM_ARNS,
94+
Collections.singletonList(logStreamArn));
95+
});
96+
} catch (IOException e) {
97+
logger.log(Level.WARNING, "Can't get ECS metadata", e);
5798
}
99+
}
58100

59-
String containerId = dockerHelper.getContainerId();
60-
if (containerId != null && !containerId.isEmpty()) {
61-
attrBuilders.put(ResourceAttributes.CONTAINER_ID, containerId);
101+
static void parseResponse(
102+
JsonParser parser, AttributesBuilder attrBuilders, LogArnBuilder logArnBuilder)
103+
throws IOException {
104+
if (!parser.isExpectedStartObjectToken()) {
105+
logger.log(Level.WARNING, "Couldn't parse ECS metadata, invalid JSON");
106+
return;
62107
}
63108

64-
return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL);
109+
while (parser.nextToken() != JsonToken.END_OBJECT) {
110+
String value = parser.nextTextValue();
111+
switch (parser.getCurrentName()) {
112+
case "DockerId":
113+
attrBuilders.put(ResourceAttributes.CONTAINER_ID, value);
114+
break;
115+
case "DockerName":
116+
attrBuilders.put(ResourceAttributes.CONTAINER_NAME, value);
117+
break;
118+
case "ContainerARN":
119+
attrBuilders.put(ResourceAttributes.AWS_ECS_CONTAINER_ARN, value);
120+
logArnBuilder.setContainerArn(value);
121+
break;
122+
case "Image":
123+
DockerImage parsedImage = DockerImage.parse(value);
124+
if (parsedImage != null) {
125+
attrBuilders.put(ResourceAttributes.CONTAINER_IMAGE_NAME, parsedImage.getRepository());
126+
attrBuilders.put(ResourceAttributes.CONTAINER_IMAGE_TAG, parsedImage.getTag());
127+
}
128+
break;
129+
case "ImageID":
130+
attrBuilders.put("aws.ecs.container.image.id", value);
131+
break;
132+
case "LogOptions":
133+
// Recursively parse LogOptions
134+
parseResponse(parser, attrBuilders, logArnBuilder);
135+
break;
136+
case "awslogs-group":
137+
attrBuilders.put(ResourceAttributes.AWS_LOG_GROUP_NAMES, value);
138+
logArnBuilder.setLogGroupName(value);
139+
break;
140+
case "awslogs-stream":
141+
attrBuilders.put(ResourceAttributes.AWS_LOG_STREAM_NAMES, value);
142+
logArnBuilder.setLogStreamName(value);
143+
break;
144+
case "awslogs-region":
145+
logArnBuilder.setRegion(value);
146+
break;
147+
case "TaskARN":
148+
attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_ARN, value);
149+
break;
150+
case "LaunchType":
151+
attrBuilders.put(ResourceAttributes.AWS_ECS_LAUNCHTYPE, value.toLowerCase());
152+
break;
153+
case "Family":
154+
attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_FAMILY, value);
155+
break;
156+
case "Revision":
157+
attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_REVISION, value);
158+
break;
159+
default:
160+
parser.skipChildren();
161+
break;
162+
}
163+
}
65164
}
66165

67-
private static boolean isOnEcs(Map<String, String> sysEnv) {
68-
return !sysEnv.getOrDefault(ECS_METADATA_KEY_V3, "").isEmpty()
69-
|| !sysEnv.getOrDefault(ECS_METADATA_KEY_V4, "").isEmpty();
166+
private EcsResource() {}
167+
168+
/**
169+
* This builder can piece together the ARN of a log group or a log stream from region, account,
170+
* group name and stream name as the ARN isn't part of the ECS metadata.
171+
*
172+
* <p>If we just set AWS_LOG_GROUP_NAMES then the CloudWatch X-Ray traces view displays "An error
173+
* occurred fetching your data". That's why it's important we set the ARN.
174+
*/
175+
private static class LogArnBuilder {
176+
177+
@Nullable String region;
178+
@Nullable String account;
179+
@Nullable String logGroupName;
180+
@Nullable String logStreamName;
181+
182+
void setRegion(@Nullable String region) {
183+
this.region = region;
184+
}
185+
186+
void setLogGroupName(@Nullable String logGroupName) {
187+
this.logGroupName = logGroupName;
188+
}
189+
190+
void setLogStreamName(@Nullable String logStreamName) {
191+
this.logStreamName = logStreamName;
192+
}
193+
194+
void setContainerArn(@Nullable String containerArn) {
195+
if (containerArn != null) {
196+
account = containerArn.split(":")[4];
197+
}
198+
}
199+
200+
Optional<String> getLogGroupArn() {
201+
if (region == null || account == null || logGroupName == null) {
202+
return Optional.empty();
203+
}
204+
205+
return Optional.of("arn:aws:logs:" + region + ":" + account + ":log-group:" + logGroupName);
206+
}
207+
208+
Optional<String> getLogStreamArn() {
209+
if (region == null || account == null || logGroupName == null || logStreamName == null) {
210+
return Optional.empty();
211+
}
212+
213+
return Optional.of(
214+
"arn:aws:logs:"
215+
+ region
216+
+ ":"
217+
+ account
218+
+ ":log-group:"
219+
+ logGroupName
220+
+ ":log-stream:"
221+
+ logStreamName);
222+
}
70223
}
71224

72-
private EcsResource() {}
225+
/** This can parse a Docker image name into its parts: repository, tag and sha256. */
226+
private static class DockerImage {
227+
228+
private static final Pattern imagePattern =
229+
Pattern.compile(
230+
"^(?<repository>([^/\\s]+/)?([^:\\s]+))(:(?<tag>[^@\\s]+))?(@sha256:(?<sha256>\\d+))?$");
231+
232+
final String repository;
233+
final String tag;
234+
235+
private DockerImage(String repository, String tag) {
236+
this.repository = repository;
237+
this.tag = tag;
238+
}
239+
240+
String getRepository() {
241+
return repository;
242+
}
243+
244+
String getTag() {
245+
return tag;
246+
}
247+
248+
@Nullable
249+
static DockerImage parse(@Nullable String image) {
250+
if (image == null || image.isEmpty()) {
251+
return null;
252+
}
253+
Matcher matcher = imagePattern.matcher(image);
254+
if (!matcher.matches()) {
255+
logger.log(Level.WARNING, "Couldn't parse image '" + image + "'");
256+
return null;
257+
}
258+
String repository = matcher.group("repository");
259+
String tag = matcher.group("tag");
260+
if (tag == null || tag.isEmpty()) {
261+
tag = "latest";
262+
}
263+
return new DockerImage(repository, tag);
264+
}
265+
}
73266
}

0 commit comments

Comments
 (0)