Skip to content

Commit 18be4f3

Browse files
authored
Move aws resource detectors from opentelemetry-java (open-telemetry#433)
1 parent 95dc58d commit 18be4f3

32 files changed

+2001
-1
lines changed

.github/component_owners.yml

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
# NOTE when adding/updating one of the component names, don't forget to update the associated
1111
# `comp:*` labels
1212
components:
13+
aws-resources:
14+
- willarmiros
1315
aws-xray:
1416
- willarmiros
1517
consistent-sampling:

aws-resources/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# OpenTelemetry AWS Resource Support
2+
3+
This module contains AWS resource detectors including Beanstalk, EC2, ECS, EKS, and Lambda.
4+
5+
## Component owners
6+
7+
- [William Armiros](https://github.com/willarmiros), AWS
8+
9+
Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).

aws-resources/build.gradle.kts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
plugins {
2+
id("otel.java-conventions")
3+
4+
// TODO: uncomment after 1.18.0 release
5+
// id("otel.publish-conventions")
6+
}
7+
8+
description = "OpenTelemetry AWS Resources Support"
9+
10+
dependencies {
11+
api("io.opentelemetry:opentelemetry-api")
12+
api("io.opentelemetry:opentelemetry-sdk")
13+
14+
implementation("io.opentelemetry:opentelemetry-semconv")
15+
16+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
17+
18+
implementation("com.fasterxml.jackson.core:jackson-core")
19+
implementation("com.squareup.okhttp3:okhttp")
20+
21+
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
22+
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
23+
24+
testImplementation("com.linecorp.armeria:armeria-junit5")
25+
testRuntimeOnly("org.bouncycastle:bcpkix-jdk15on")
26+
testImplementation("com.google.guava:guava")
27+
testImplementation("org.skyscreamer:jsonassert")
28+
}

aws-resources/gradle.properties

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# TODO: uncomment when ready to mark as stable
2+
# otel.stable=true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.aws.resource;
7+
8+
import com.fasterxml.jackson.core.JsonFactory;
9+
import com.fasterxml.jackson.core.JsonParser;
10+
import com.fasterxml.jackson.core.JsonToken;
11+
import io.opentelemetry.api.common.Attributes;
12+
import io.opentelemetry.api.common.AttributesBuilder;
13+
import io.opentelemetry.sdk.resources.Resource;
14+
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
15+
import java.io.File;
16+
import java.io.IOException;
17+
import java.util.logging.Level;
18+
import java.util.logging.Logger;
19+
20+
/**
21+
* A factory for a {@link Resource} which provides information about the current EC2 instance if
22+
* running on AWS Elastic Beanstalk.
23+
*/
24+
public final class BeanstalkResource {
25+
26+
private static final Logger logger = Logger.getLogger(BeanstalkResource.class.getName());
27+
28+
private static final String DEVELOPMENT_ID = "deployment_id";
29+
private static final String VERSION_LABEL = "version_label";
30+
private static final String ENVIRONMENT_NAME = "environment_name";
31+
private static final String BEANSTALK_CONF_PATH = "/var/elasticbeanstalk/xray/environment.conf";
32+
private static final JsonFactory JSON_FACTORY = new JsonFactory();
33+
34+
private static final Resource INSTANCE = buildResource();
35+
36+
/**
37+
* Returns a factory for a {@link Resource} which provides information about the current EC2
38+
* instance if running on AWS Elastic Beanstalk.
39+
*/
40+
public static Resource get() {
41+
return INSTANCE;
42+
}
43+
44+
private static Resource buildResource() {
45+
return buildResource(BEANSTALK_CONF_PATH);
46+
}
47+
48+
// Visible for testing
49+
static Resource buildResource(String configPath) {
50+
File configFile = new File(configPath);
51+
if (!configFile.exists()) {
52+
return Resource.empty();
53+
}
54+
55+
AttributesBuilder attrBuilders = Attributes.builder();
56+
try (JsonParser parser = JSON_FACTORY.createParser(configFile)) {
57+
parser.nextToken();
58+
59+
if (!parser.isExpectedStartObjectToken()) {
60+
logger.log(Level.WARNING, "Invalid Beanstalk config: ", configPath);
61+
return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL);
62+
}
63+
64+
while (parser.nextToken() != JsonToken.END_OBJECT) {
65+
parser.nextValue();
66+
String value = parser.getText();
67+
switch (parser.getCurrentName()) {
68+
case DEVELOPMENT_ID:
69+
attrBuilders.put(ResourceAttributes.SERVICE_INSTANCE_ID, value);
70+
break;
71+
case VERSION_LABEL:
72+
attrBuilders.put(ResourceAttributes.SERVICE_VERSION, value);
73+
break;
74+
case ENVIRONMENT_NAME:
75+
attrBuilders.put(ResourceAttributes.SERVICE_NAMESPACE, value);
76+
break;
77+
default:
78+
parser.skipChildren();
79+
}
80+
}
81+
} catch (IOException e) {
82+
logger.log(Level.WARNING, "Could not parse Beanstalk config.", e);
83+
return Resource.empty();
84+
}
85+
86+
attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS);
87+
attrBuilders.put(
88+
ResourceAttributes.CLOUD_PLATFORM,
89+
ResourceAttributes.CloudPlatformValues.AWS_ELASTIC_BEANSTALK);
90+
91+
return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL);
92+
}
93+
94+
private BeanstalkResource() {}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.aws.resource;
7+
8+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
9+
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
10+
import io.opentelemetry.sdk.resources.Resource;
11+
12+
/** {@link ResourceProvider} for automatically configuring {@link BeanstalkResource}. */
13+
public final class BeanstalkResourceProvider implements ResourceProvider {
14+
@Override
15+
public Resource createResource(ConfigProperties config) {
16+
return BeanstalkResource.get();
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.aws.resource;
7+
8+
import java.io.BufferedReader;
9+
import java.io.FileNotFoundException;
10+
import java.io.FileReader;
11+
import java.io.IOException;
12+
import java.util.logging.Level;
13+
import java.util.logging.Logger;
14+
15+
class DockerHelper {
16+
17+
private static final Logger logger = Logger.getLogger(DockerHelper.class.getName());
18+
private static final int CONTAINER_ID_LENGTH = 64;
19+
private static final String DEFAULT_CGROUP_PATH = "/proc/self/cgroup";
20+
21+
private final String cgroupPath;
22+
23+
DockerHelper() {
24+
this(DEFAULT_CGROUP_PATH);
25+
}
26+
27+
// Visible for testing
28+
DockerHelper(String cgroupPath) {
29+
this.cgroupPath = cgroupPath;
30+
}
31+
32+
/**
33+
* Get docker container id from local cgroup file.
34+
*
35+
* @return docker container ID. Empty string if it can`t be found.
36+
*/
37+
@SuppressWarnings("DefaultCharset")
38+
public String getContainerId() {
39+
try (BufferedReader br = new BufferedReader(new FileReader(cgroupPath))) {
40+
String line;
41+
while ((line = br.readLine()) != null) {
42+
if (line.length() > CONTAINER_ID_LENGTH) {
43+
return line.substring(line.length() - CONTAINER_ID_LENGTH);
44+
}
45+
}
46+
} catch (FileNotFoundException e) {
47+
logger.log(Level.WARNING, "Failed to read container id, cgroup file does not exist.");
48+
} catch (IOException e) {
49+
logger.log(Level.WARNING, "Unable to read container id: " + e.getMessage());
50+
}
51+
52+
return "";
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.aws.resource;
7+
8+
import com.fasterxml.jackson.core.JsonFactory;
9+
import com.fasterxml.jackson.core.JsonParser;
10+
import com.fasterxml.jackson.core.JsonToken;
11+
import io.opentelemetry.api.common.Attributes;
12+
import io.opentelemetry.api.common.AttributesBuilder;
13+
import io.opentelemetry.sdk.resources.Resource;
14+
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
15+
import java.io.IOException;
16+
import java.net.MalformedURLException;
17+
import java.net.URL;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.logging.Level;
21+
import java.util.logging.Logger;
22+
23+
/**
24+
* A factory for a {@link Resource} which provides information about the current EC2 instance if
25+
* running on AWS EC2.
26+
*/
27+
public final class Ec2Resource {
28+
29+
private static final Logger logger = Logger.getLogger(Ec2Resource.class.getName());
30+
31+
private static final JsonFactory JSON_FACTORY = new JsonFactory();
32+
33+
private static final String DEFAULT_IMDS_ENDPOINT = "169.254.169.254";
34+
35+
private static final Resource INSTANCE = buildResource();
36+
37+
/**
38+
* Returns a @link Resource} which provides information about the current EC2 instance if running
39+
* on AWS EC2.
40+
*/
41+
public static Resource get() {
42+
return INSTANCE;
43+
}
44+
45+
private static Resource buildResource() {
46+
// This property is only for testing e.g., with a mock IMDS server and never in production so we
47+
// just
48+
// read from a system property. This is similar to the AWS SDK.
49+
return buildResource(
50+
System.getProperty("otel.aws.imds.endpointOverride", DEFAULT_IMDS_ENDPOINT));
51+
}
52+
53+
// Visible for testing
54+
static Resource buildResource(String endpoint) {
55+
String urlBase = "http://" + endpoint;
56+
URL identityDocumentUrl;
57+
URL hostnameUrl;
58+
URL tokenUrl;
59+
try {
60+
identityDocumentUrl = new URL(urlBase + "/latest/dynamic/instance-identity/document");
61+
hostnameUrl = new URL(urlBase + "/latest/meta-data/hostname");
62+
tokenUrl = new URL(urlBase + "/latest/api/token");
63+
} catch (MalformedURLException e) {
64+
// Can only happen when overriding the endpoint in testing so just throw.
65+
throw new IllegalArgumentException("Illegal endpoint: " + endpoint, e);
66+
}
67+
68+
String token = fetchToken(tokenUrl);
69+
70+
// If token is empty, either IMDSv2 isn't enabled or an unexpected failure happened. We can
71+
// still get data if IMDSv1 is enabled.
72+
String identity = fetchIdentity(identityDocumentUrl, token);
73+
if (identity.isEmpty()) {
74+
// If no identity document, assume we are not actually running on EC2.
75+
return Resource.empty();
76+
}
77+
78+
String hostname = fetchHostname(hostnameUrl, token);
79+
80+
AttributesBuilder attrBuilders = Attributes.builder();
81+
attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS);
82+
attrBuilders.put(
83+
ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.AWS_EC2);
84+
85+
try (JsonParser parser = JSON_FACTORY.createParser(identity)) {
86+
parser.nextToken();
87+
88+
if (!parser.isExpectedStartObjectToken()) {
89+
throw new IOException("Invalid JSON:" + identity);
90+
}
91+
92+
while (parser.nextToken() != JsonToken.END_OBJECT) {
93+
String value = parser.nextTextValue();
94+
switch (parser.getCurrentName()) {
95+
case "instanceId":
96+
attrBuilders.put(ResourceAttributes.HOST_ID, value);
97+
break;
98+
case "availabilityZone":
99+
attrBuilders.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, value);
100+
break;
101+
case "instanceType":
102+
attrBuilders.put(ResourceAttributes.HOST_TYPE, value);
103+
break;
104+
case "imageId":
105+
attrBuilders.put(ResourceAttributes.HOST_IMAGE_ID, value);
106+
break;
107+
case "accountId":
108+
attrBuilders.put(ResourceAttributes.CLOUD_ACCOUNT_ID, value);
109+
break;
110+
case "region":
111+
attrBuilders.put(ResourceAttributes.CLOUD_REGION, value);
112+
break;
113+
default:
114+
parser.skipChildren();
115+
}
116+
}
117+
} catch (IOException e) {
118+
logger.log(Level.WARNING, "Could not parse identity document, resource not filled.", e);
119+
return Resource.empty();
120+
}
121+
122+
attrBuilders.put(ResourceAttributes.HOST_NAME, hostname);
123+
124+
return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL);
125+
}
126+
127+
private static String fetchToken(URL tokenUrl) {
128+
return fetchString("PUT", tokenUrl, "", /* includeTtl= */ true);
129+
}
130+
131+
private static String fetchIdentity(URL identityDocumentUrl, String token) {
132+
return fetchString("GET", identityDocumentUrl, token, /* includeTtl= */ false);
133+
}
134+
135+
private static String fetchHostname(URL hostnameUrl, String token) {
136+
return fetchString("GET", hostnameUrl, token, /* includeTtl= */ false);
137+
}
138+
139+
// Generic HTTP fetch function for IMDS.
140+
private static String fetchString(String httpMethod, URL url, String token, boolean includeTtl) {
141+
SimpleHttpClient client = new SimpleHttpClient();
142+
Map<String, String> headers = new HashMap<>();
143+
144+
if (includeTtl) {
145+
headers.put("X-aws-ec2-metadata-token-ttl-seconds", "60");
146+
}
147+
if (!token.isEmpty()) {
148+
headers.put("X-aws-ec2-metadata-token", token);
149+
}
150+
151+
return client.fetchString(httpMethod, url.toString(), headers, null);
152+
}
153+
154+
private Ec2Resource() {}
155+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.aws.resource;
7+
8+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
9+
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
10+
import io.opentelemetry.sdk.resources.Resource;
11+
12+
/** {@link ResourceProvider} for automatically configuring {@link Ec2Resource}. */
13+
public final class Ec2ResourceProvider implements ResourceProvider {
14+
@Override
15+
public Resource createResource(ConfigProperties config) {
16+
return Ec2Resource.get();
17+
}
18+
}

0 commit comments

Comments
 (0)