Skip to content

Commit 2489a57

Browse files
authored
Added log correlation to ECS plugin (#263)
* added log correlation to ECS plugin * comments and ci
1 parent e3dadf0 commit 2489a57

6 files changed

Lines changed: 463 additions & 107 deletions

File tree

aws-xray-recorder-sdk-core/src/main/java/com/amazonaws/xray/plugins/EC2MetadataFetcher.java

Lines changed: 3 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,14 @@
1818
import com.fasterxml.jackson.core.JsonFactory;
1919
import com.fasterxml.jackson.core.JsonParser;
2020
import com.fasterxml.jackson.core.JsonToken;
21-
import java.io.ByteArrayOutputStream;
2221
import java.io.IOException;
23-
import java.io.InputStream;
24-
import java.io.UnsupportedEncodingException;
25-
import java.net.HttpURLConnection;
2622
import java.net.MalformedURLException;
27-
import java.net.ProtocolException;
28-
import java.net.SocketTimeoutException;
2923
import java.net.URL;
30-
import java.nio.charset.StandardCharsets;
3124
import java.util.Collections;
3225
import java.util.HashMap;
3326
import java.util.Map;
3427
import org.apache.commons.logging.Log;
3528
import org.apache.commons.logging.LogFactory;
36-
import org.checkerframework.checker.nullness.qual.Nullable;
3729

3830
class EC2MetadataFetcher {
3931
private static final Log logger = LogFactory.getLog(EC2MetadataFetcher.class);
@@ -47,8 +39,7 @@ enum EC2Metadata {
4739
AMI_ID,
4840
}
4941

50-
private static final int CONNECT_TIMEOUT_MILLIS = 100;
51-
private static final int READ_TIMEOUT_MILLIS = 1000;
42+
private static final String METADATA_SERVICE_NAME = "IMDS";
5243
private static final String DEFAULT_IMDS_ENDPOINT = "169.254.169.254";
5344

5445
private final URL identityDocumentUrl;
@@ -120,86 +111,11 @@ Map<EC2Metadata, String> fetch() {
120111
}
121112

122113
private String fetchToken() {
123-
return fetchString("PUT", tokenUrl, "", true);
114+
return MetadataUtils.fetchString("PUT", tokenUrl, "", true, METADATA_SERVICE_NAME);
124115
}
125116

126117
private String fetchIdentity(String token) {
127-
return fetchString("GET", identityDocumentUrl, token, false);
128-
}
129-
130-
// Generic HTTP fetch function for IMDS.
131-
private static String fetchString(String httpMethod, URL url, String token, boolean includeTtl) {
132-
final HttpURLConnection connection;
133-
try {
134-
connection = (HttpURLConnection) url.openConnection();
135-
} catch (Exception e) {
136-
logger.debug("Error connecting to IMDS.", e);
137-
return "";
138-
}
139-
140-
try {
141-
connection.setRequestMethod(httpMethod);
142-
} catch (ProtocolException e) {
143-
logger.warn("Unknown HTTP method, this is a programming bug.", e);
144-
return "";
145-
}
146-
147-
connection.setConnectTimeout(CONNECT_TIMEOUT_MILLIS);
148-
connection.setReadTimeout(READ_TIMEOUT_MILLIS);
149-
150-
if (includeTtl) {
151-
connection.setRequestProperty("X-aws-ec2-metadata-token-ttl-seconds", "60");
152-
}
153-
if (!token.isEmpty()) {
154-
connection.setRequestProperty("X-aws-ec2-metadata-token", token);
155-
}
156-
157-
final int responseCode;
158-
try {
159-
responseCode = connection.getResponseCode();
160-
} catch (Exception e) {
161-
if (e instanceof SocketTimeoutException) {
162-
logger.debug("Timed out trying to connect to IMDS, likely not operating in EC2 environment");
163-
} else {
164-
logger.debug("Error connecting to IMDS.", e);
165-
}
166-
return "";
167-
}
168-
169-
if (responseCode != 200) {
170-
logger.warn("Error reponse from IMDS: code (" + responseCode + ") text " + readResponseString(connection));
171-
}
172-
173-
return readResponseString(connection).trim();
174-
}
175-
176-
private static String readResponseString(HttpURLConnection connection) {
177-
ByteArrayOutputStream os = new ByteArrayOutputStream();
178-
try (InputStream is = connection.getInputStream()) {
179-
readTo(is, os);
180-
} catch (IOException e) {
181-
// Only best effort read if we can.
182-
}
183-
try (InputStream is = connection.getErrorStream()) {
184-
readTo(is, os);
185-
} catch (IOException e) {
186-
// Only best effort read if we can.
187-
}
188-
try {
189-
return os.toString(StandardCharsets.UTF_8.name());
190-
} catch (UnsupportedEncodingException e) {
191-
throw new IllegalStateException("UTF-8 not supported can't happen.");
192-
}
193-
}
194-
195-
private static void readTo(@Nullable InputStream is, ByteArrayOutputStream os) throws IOException {
196-
if (is == null) {
197-
return;
198-
}
199-
int b;
200-
while ((b = is.read()) != -1) {
201-
os.write(b);
202-
}
118+
return MetadataUtils.fetchString("GET", identityDocumentUrl, token, false, METADATA_SERVICE_NAME);
203119
}
204120

205121
private static String getEndpoint() {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amazonaws.xray.plugins;
17+
18+
import com.fasterxml.jackson.core.JsonFactory;
19+
import com.fasterxml.jackson.core.JsonParser;
20+
import com.fasterxml.jackson.core.JsonToken;
21+
import java.io.IOException;
22+
import java.net.MalformedURLException;
23+
import java.net.URL;
24+
import java.util.Collections;
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
import org.apache.commons.logging.Log;
28+
import org.apache.commons.logging.LogFactory;
29+
import org.checkerframework.checker.nullness.qual.Nullable;
30+
31+
class ECSMetadataFetcher {
32+
private static final Log logger = LogFactory.getLog(ECSMetadataFetcher.class);
33+
34+
private static final String METADATA_SERVICE_NAME = "TMDE";
35+
private static final JsonFactory JSON_FACTORY = new JsonFactory();
36+
37+
@Nullable
38+
private final URL containerUrl;
39+
40+
// TODO: Record additional attributes in runtime context from Task Metadata Endpoint
41+
enum ECSContainerMetadata {
42+
LOG_DRIVER,
43+
LOG_GROUP_REGION,
44+
LOG_GROUP_NAME,
45+
CONTAINER_ARN,
46+
}
47+
48+
ECSMetadataFetcher(@Nullable String endpoint) {
49+
if (endpoint == null) {
50+
this.containerUrl = null;
51+
return;
52+
}
53+
54+
try {
55+
this.containerUrl = new URL(endpoint);
56+
} catch (MalformedURLException e) {
57+
throw new IllegalArgumentException("Illegal endpoint: " + endpoint);
58+
}
59+
}
60+
61+
Map<ECSContainerMetadata, String> fetchContainer() {
62+
if (this.containerUrl == null) {
63+
return Collections.emptyMap();
64+
}
65+
66+
String metadata = MetadataUtils.fetchString("GET", this.containerUrl, "", false, METADATA_SERVICE_NAME);
67+
68+
Map<ECSContainerMetadata, String> result = new HashMap<>();
69+
try (JsonParser parser = JSON_FACTORY.createParser(metadata)) {
70+
parser.nextToken();
71+
parseContainerJson(parser, result);
72+
} catch (IOException e) {
73+
logger.warn("Could not parse container metadata.", e);
74+
return Collections.emptyMap();
75+
}
76+
77+
// This means the document didn't have all the metadata fields we wanted.
78+
if (result.size() != ECSContainerMetadata.values().length) {
79+
logger.debug("Container metadata response missing metadata: " + metadata);
80+
}
81+
82+
return Collections.unmodifiableMap(result);
83+
}
84+
85+
// Helper method to shallow-parse a JSON object, assuming the parser is located at the start of an object,
86+
// and record the desired fields in the result map in-place
87+
private void parseContainerJson(JsonParser parser, Map<ECSContainerMetadata, String> result) throws IOException {
88+
if (!parser.isExpectedStartObjectToken()) {
89+
logger.warn("Container metadata endpoint returned invalid JSON");
90+
return;
91+
}
92+
93+
while (parser.nextToken() != JsonToken.END_OBJECT) {
94+
String value = parser.nextTextValue();
95+
switch (parser.getCurrentName()) {
96+
case "LogDriver":
97+
result.put(ECSContainerMetadata.LOG_DRIVER, value);
98+
break;
99+
case "ContainerARN":
100+
result.put(ECSContainerMetadata.CONTAINER_ARN, value);
101+
break;
102+
case "awslogs-group":
103+
result.put(ECSContainerMetadata.LOG_GROUP_NAME, value);
104+
break;
105+
case "awslogs-region":
106+
result.put(ECSContainerMetadata.LOG_GROUP_REGION, value);
107+
break;
108+
case "LogOptions":
109+
parseContainerJson(parser, result); // Parse the LogOptions object for log fields
110+
break;
111+
default:
112+
parser.skipChildren();
113+
}
114+
if (result.size() == ECSContainerMetadata.values().length) {
115+
return;
116+
}
117+
}
118+
}
119+
}

aws-xray-recorder-sdk-core/src/main/java/com/amazonaws/xray/plugins/ECSPlugin.java

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515

1616
package com.amazonaws.xray.plugins;
1717

18+
import com.amazonaws.xray.entities.AWSLogReference;
1819
import com.amazonaws.xray.utils.DockerUtils;
1920
import java.io.IOException;
2021
import java.net.InetAddress;
2122
import java.net.UnknownHostException;
2223
import java.util.HashMap;
24+
import java.util.HashSet;
2325
import java.util.Map;
26+
import java.util.Set;
2427
import org.apache.commons.logging.Log;
2528
import org.apache.commons.logging.LogFactory;
2629
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -38,29 +41,43 @@ public class ECSPlugin implements Plugin {
3841
private static final Log logger = LogFactory.getLog(ECSPlugin.class);
3942

4043
private static final String SERVICE_NAME = "ecs";
41-
private static final String ECS_METADATA_KEY = "ECS_CONTAINER_METADATA_URI";
42-
private static final String HTTP_PREFIX = "http://";
44+
private static final String ECS_METADATA_V3_KEY = "ECS_CONTAINER_METADATA_URI";
45+
private static final String ECS_METADATA_V4_KEY = "ECS_CONTAINER_METADATA_URI_V4";
4346
private static final String CONTAINER_ID_KEY = "container_id";
47+
private static final String CONTAINER_NAME_KEY = "container";
48+
private static final String CONTAINER_ARN_KEY = "container_arn";
4449

50+
private final ECSMetadataFetcher fetcher;
4551
private final HashMap<String, @Nullable Object> runtimeContext;
4652
private final DockerUtils dockerUtils;
53+
private final Set<AWSLogReference> logReferences;
54+
private final Map<ECSMetadataFetcher.ECSContainerMetadata, String> containerMetadata;
4755

56+
@SuppressWarnings("nullness:method.invocation.invalid")
4857
public ECSPlugin() {
4958
runtimeContext = new HashMap<>();
5059
dockerUtils = new DockerUtils();
60+
logReferences = new HashSet<>();
61+
fetcher = new ECSMetadataFetcher(getTmdeFromEnv());
62+
containerMetadata = this.fetcher.fetchContainer();
63+
}
64+
65+
// Exposed for testing
66+
ECSPlugin(ECSMetadataFetcher fetcher) {
67+
runtimeContext = new HashMap<>();
68+
dockerUtils = new DockerUtils();
69+
logReferences = new HashSet<>();
70+
this.fetcher = fetcher;
71+
containerMetadata = this.fetcher.fetchContainer();
5172
}
5273

5374
/**
5475
* Returns true if the environment variable added by ECS is present and contains a valid URI
5576
*/
5677
@Override
5778
public boolean isEnabled() {
58-
String ecsMetadataUri = System.getenv(ECS_METADATA_KEY);
59-
if (ecsMetadataUri == null) {
60-
return false;
61-
}
62-
63-
return ecsMetadataUri.startsWith(HTTP_PREFIX);
79+
String ecsMetadataUri = getTmdeFromEnv();
80+
return ecsMetadataUri != null && ecsMetadataUri.startsWith("http://");
6481
}
6582

6683
@Override
@@ -70,7 +87,7 @@ public String getServiceName() {
7087

7188
public void populateRuntimeContext() {
7289
try {
73-
runtimeContext.put("container", InetAddress.getLocalHost().getHostName());
90+
runtimeContext.put(CONTAINER_NAME_KEY, InetAddress.getLocalHost().getHostName());
7491
} catch (UnknownHostException uhe) {
7592
logger.error("Could not get docker container ID from hostname.", uhe);
7693
}
@@ -80,6 +97,10 @@ public void populateRuntimeContext() {
8097
} catch (IOException e) {
8198
logger.error("Failed to read full container ID from container instance.", e);
8299
}
100+
101+
if (containerMetadata.containsKey(ECSMetadataFetcher.ECSContainerMetadata.CONTAINER_ARN)) {
102+
runtimeContext.put(CONTAINER_ARN_KEY, containerMetadata.get(ECSMetadataFetcher.ECSContainerMetadata.CONTAINER_ARN));
103+
}
83104
}
84105

85106
@Override
@@ -88,6 +109,34 @@ public void populateRuntimeContext() {
88109
return runtimeContext;
89110
}
90111

112+
@Override
113+
public Set<AWSLogReference> getLogReferences() {
114+
if (logReferences.isEmpty()) {
115+
populateLogReferences();
116+
}
117+
118+
return logReferences;
119+
}
120+
121+
// See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids
122+
private void populateLogReferences() {
123+
String logGroup = containerMetadata.get(ECSMetadataFetcher.ECSContainerMetadata.LOG_GROUP_NAME);
124+
if (logGroup == null) {
125+
return;
126+
}
127+
AWSLogReference logReference = new AWSLogReference();
128+
logReference.setLogGroup(logGroup);
129+
130+
String logRegion = containerMetadata.get(ECSMetadataFetcher.ECSContainerMetadata.LOG_GROUP_REGION);
131+
String containerArn = containerMetadata.get(ECSMetadataFetcher.ECSContainerMetadata.CONTAINER_ARN);
132+
String logAccount = containerArn != null ? containerArn.split(":")[4] : null;
133+
134+
if (logRegion != null && logAccount != null) {
135+
logReference.setArn("arn:aws:logs:" + logRegion + ":" + logAccount + ":log-group:" + logGroup);
136+
}
137+
logReferences.add(logReference);
138+
}
139+
91140
@Override
92141
public String getOrigin() {
93142
return ORIGIN;
@@ -109,4 +158,17 @@ public boolean equals(@Nullable Object o) {
109158
public int hashCode() {
110159
return this.getOrigin().hashCode();
111160
}
161+
162+
/**
163+
* @return V4 Metadata endpoint if present, otherwise V3 endpoint if present, otherwise null
164+
*/
165+
@Nullable
166+
private String getTmdeFromEnv() {
167+
String ecsMetadataUri = System.getenv(ECS_METADATA_V4_KEY);
168+
if (ecsMetadataUri == null) {
169+
ecsMetadataUri = System.getenv(ECS_METADATA_V3_KEY);
170+
}
171+
172+
return ecsMetadataUri;
173+
}
112174
}

0 commit comments

Comments
 (0)