5
5
6
6
package io .opentelemetry .sdk .extension .aws .resource ;
7
7
8
+ import com .fasterxml .jackson .core .JsonFactory ;
9
+ import com .fasterxml .jackson .core .JsonParser ;
10
+ import com .fasterxml .jackson .core .JsonToken ;
8
11
import io .opentelemetry .api .common .Attributes ;
9
12
import io .opentelemetry .api .common .AttributesBuilder ;
10
13
import io .opentelemetry .sdk .resources .Resource ;
11
14
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 ;
14
17
import java .util .Map ;
18
+ import java .util .Optional ;
15
19
import java .util .logging .Level ;
16
20
import java .util .logging .Logger ;
21
+ import java .util .regex .Matcher ;
22
+ import java .util .regex .Pattern ;
23
+ import javax .annotation .Nullable ;
17
24
18
25
/**
19
26
* A factory for a {@link Resource} which provides information about the current ECS container if
20
27
* running on AWS ECS.
21
28
*/
22
29
public final class EcsResource {
23
30
private static final Logger logger = Logger .getLogger (EcsResource .class .getName ());
24
-
31
+ private static final JsonFactory JSON_FACTORY = new JsonFactory ();
25
32
private static final String ECS_METADATA_KEY_V4 = "ECS_CONTAINER_METADATA_URI_V4" ;
26
33
private static final String ECS_METADATA_KEY_V3 = "ECS_CONTAINER_METADATA_URI" ;
27
34
@@ -36,38 +43,224 @@ public static Resource get() {
36
43
}
37
44
38
45
private static Resource buildResource () {
39
- return buildResource (System .getenv (), new DockerHelper ());
46
+ return buildResource (System .getenv (), new SimpleHttpClient ());
40
47
}
41
48
42
49
// 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 );
46
61
}
62
+ // Not running on ECS
63
+ return Resource .empty ();
64
+ }
47
65
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
+ }
49
72
attrBuilders .put (ResourceAttributes .CLOUD_PROVIDER , ResourceAttributes .CloudProviderValues .AWS );
50
73
attrBuilders .put (
51
74
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 );
57
98
}
99
+ }
58
100
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 ;
62
107
}
63
108
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
+ }
65
164
}
66
165
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
+ }
70
223
}
71
224
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
+ }
73
266
}
0 commit comments