Skip to content

Commit 92f0e66

Browse files
Merge pull request #12 from open-o11y/Ec2Detector
Add EC2 Detector
2 parents 08e11c1 + 22f3d46 commit 92f0e66

File tree

2 files changed

+396
-0
lines changed

2 files changed

+396
-0
lines changed

detectors/Ec2Detector.php

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* Copyright The OpenTelemetry Authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* https://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
namespace Detectors\Aws;
22+
23+
use GuzzleHttp\Client;
24+
use GuzzleHttp\Exception\RequestException;
25+
use GuzzleHttp\Psr7\Request;
26+
use OpenTelemetry\Sdk\Resource\ResourceConstants;
27+
use OpenTelemetry\Sdk\Resource\ResourceInfo;
28+
use OpenTelemetry\Sdk\Trace\Attributes;
29+
30+
/**
31+
* The AwsEc2Detector can be used to detect if a process is running in AWS EC2
32+
* and return a Resource populated with metadata about the EC2
33+
* instance. Returns an empty Resource if detection fails.
34+
*/
35+
class Ec2Detector
36+
{
37+
private const SCHEME = 'http://';
38+
private const AWS_IDMS_ENDPOINT = '169.254.169.254';
39+
private const AWS_INSTANCE_TOKEN_DOCUMENT_PATH = '/latest/api/token';
40+
private const AWS_INSTANCE_IDENTITY_DOCUMENT_PATH = '/latest/dynamic/instance-identity/document';
41+
private const AWS_INSTANCE_HOST_DOCUMENT_PATH = '/latest/meta-data/hostname';
42+
private const AWS_METADATA_TTL_HEADER = 'X-aws-ec2-metadata-token-ttl-seconds';
43+
private const AWS_METADATA_TOKEN_HEADER = 'X-aws-ec2-metadata-token';
44+
private const MILLISECOND_TIME_OUT = 1000;
45+
private const CLOUD_PROVIDER = 'aws';
46+
47+
private $guzzle;
48+
49+
public function __construct(Client $guzzle)
50+
{
51+
$this->guzzle = $guzzle;
52+
}
53+
54+
/**
55+
* Attempts to connect and obtain an AWS instance Identity document. If the
56+
* connection is succesful it returns a Resource
57+
* populated with instance metadata. Returns an empty Resource
58+
* if the connection or parsing of the identity document fails.
59+
*
60+
*/
61+
public function detect()
62+
{
63+
try {
64+
$token = $this->fetchToken();
65+
66+
$hostName = $this->fetchHostname($token);
67+
68+
$identitiesJson = $this->fetchIdentity($token);
69+
70+
if (!$token || !$identitiesJson) {
71+
return ResourceInfo::emptyResource();
72+
}
73+
74+
$attributes = new Attributes();
75+
76+
foreach ($identitiesJson as $key => $value) {
77+
switch ($key) {
78+
case 'instanceId':
79+
$attributes->setAttribute(ResourceConstants::HOST_ID, $value);
80+
81+
break;
82+
case 'availabilityZone':
83+
$attributes->setAttribute(ResourceConstants::CLOUD_ZONE, $value);
84+
85+
break;
86+
case 'instanceType':
87+
$attributes->setAttribute(ResourceConstants::HOST_TYPE, $value);
88+
89+
break;
90+
case 'imageId':
91+
$attributes->setAttribute(ResourceConstants::HOST_IMAGE_ID, $value);
92+
93+
break;
94+
case 'accountId':
95+
$attributes->setAttribute(ResourceConstants::CLOUD_ACCOUNT_ID, $value);
96+
97+
break;
98+
case 'region':
99+
$attributes->setAttribute(ResourceConstants::CLOUD_REGION, $value);
100+
101+
break;
102+
}
103+
}
104+
105+
$attributes->setAttribute(ResourceConstants::HOST_HOSTNAME, $hostName);
106+
$attributes->setAttribute(ResourceConstants::CLOUD_PROVIDER, self::CLOUD_PROVIDER);
107+
108+
return ResourceInfo::create($attributes);
109+
} catch (\Throwable $e) {
110+
//TODO: add 'Process is not running on K8S when logging is added
111+
return ResourceInfo::emptyResource();
112+
}
113+
}
114+
115+
private function fetchToken()
116+
{
117+
return $this->request(
118+
'PUT',
119+
self::AWS_INSTANCE_TOKEN_DOCUMENT_PATH,
120+
[self::AWS_METADATA_TTL_HEADER => '60']
121+
);
122+
}
123+
124+
private function fetchIdentity(String $token)
125+
{
126+
$body = $this->request(
127+
'GET',
128+
self::AWS_INSTANCE_IDENTITY_DOCUMENT_PATH,
129+
[self::AWS_METADATA_TOKEN_HEADER => $token]
130+
);
131+
132+
$json = json_decode($body, true);
133+
134+
if (isset($json)) {
135+
return $json;
136+
}
137+
138+
return null;
139+
}
140+
141+
private function fetchHostname(String $token)
142+
{
143+
return $this->request(
144+
'GET',
145+
self::AWS_INSTANCE_HOST_DOCUMENT_PATH,
146+
[self::AWS_METADATA_TOKEN_HEADER => $token]
147+
);
148+
}
149+
150+
/**
151+
* Function to create a request for any of the given
152+
* fetch functions.
153+
*/
154+
private function request($method, $path, $header)
155+
{
156+
$client = $this->guzzle;
157+
158+
try {
159+
$response = $client->request(
160+
$method,
161+
self::SCHEME . self::AWS_IDMS_ENDPOINT . $path,
162+
[
163+
'headers' => $header,
164+
'timeout' => self::MILLISECOND_TIME_OUT,
165+
]
166+
);
167+
168+
$body = $response->getBody()->getContents();
169+
$responseCode = $response->getStatusCode();
170+
171+
if (!empty($body) && $responseCode < 300 && $responseCode >= 200) {
172+
return $body;
173+
}
174+
175+
return null;
176+
} catch (RequestException $e) {
177+
// TODO: add log for exception. The code below
178+
// provides the exception thrown:
179+
// echo Psr7\Message::toString($e->getRequest());
180+
// if ($e->hasResponse()) {
181+
// echo Psr7\Message::toString($e->getResponse());
182+
// }
183+
return null;
184+
}
185+
}
186+
}

tests/unit/Ec2DetectorTest.php

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Detectors\Aws\Ec2Detector;
6+
use GuzzleHttp\Client;
7+
use GuzzleHttp\Handler\MockHandler;
8+
use GuzzleHttp\HandlerStack;
9+
use GuzzleHttp\Psr7\Response;
10+
use OpenTelemetry\Sdk\Resource\ResourceConstants;
11+
use OpenTelemetry\Sdk\Resource\ResourceInfo;
12+
use OpenTelemetry\Sdk\Trace\Attributes;
13+
use PHPUnit\Framework\TestCase;
14+
15+
class Ec2DetectorTest extends TestCase
16+
{
17+
private const MOCK_TOKEN_RESPONSE = 'my-token';
18+
private const MOCK_HOSTNAME = 'my-hostname';
19+
private const MOCK_IDENTITY = '{
20+
"instanceId": "my-instance-id",
21+
"instanceType": "my-instance-type",
22+
"accountId": "my-account-id",
23+
"region": "my-region",
24+
"availabilityZone": "my-zone",
25+
"imageId": "image-id"
26+
}';
27+
28+
private const MOCK_IDENTITY_INCOMPLETE = '{
29+
"instanceId": "my-instance-id",
30+
"instanceType": "my-instance-type",
31+
"availabilityZone": "my-zone",
32+
"imageId": "image-id"
33+
}';
34+
35+
private const HOST_ID = 'my-instance-id';
36+
private const CLOUD_ZONE = 'my-zone';
37+
private const HOST_TYPE = 'my-instance-type';
38+
private const HOST_IMAGE_ID = 'image-id';
39+
private const CLOUD_ACCOUNT_ID = 'my-account-id';
40+
private const CLOUD_REGION = 'my-region';
41+
private const CLOUD_PROVIDER = 'aws';
42+
43+
/**
44+
* @test
45+
*/
46+
public function TestValidEc2()
47+
{
48+
$mockGuzzle = new MockHandler([
49+
//Fetch token response
50+
new Response(200, ['Foo' => 'Bar'], self::MOCK_TOKEN_RESPONSE),
51+
// Fetch hostName response
52+
new Response(200, ['Foo' => 'Bar'], self::MOCK_HOSTNAME),
53+
// Fetch identities reponse
54+
new Response(200, ['Foo' => 'Bar'], self::MOCK_IDENTITY),
55+
]);
56+
57+
$handlerStack = HandlerStack::create($mockGuzzle);
58+
$client = new Client(['handler' => $handlerStack]);
59+
60+
$detector = new Ec2Detector($client);
61+
62+
$this->assertEquals(ResourceInfo::create(
63+
new Attributes(
64+
[
65+
ResourceConstants::HOST_ID => self::HOST_ID,
66+
ResourceConstants::CLOUD_ZONE => self::CLOUD_ZONE,
67+
ResourceConstants::HOST_TYPE => self::HOST_TYPE,
68+
ResourceConstants::HOST_IMAGE_ID => self::HOST_IMAGE_ID,
69+
ResourceConstants::CLOUD_ACCOUNT_ID => self::CLOUD_ACCOUNT_ID,
70+
ResourceConstants::CLOUD_REGION => self::CLOUD_REGION,
71+
ResourceConstants::HOST_HOSTNAME => self::MOCK_HOSTNAME,
72+
ResourceConstants::CLOUD_PROVIDER => self::CLOUD_PROVIDER,
73+
]
74+
)
75+
), $detector->detect());
76+
}
77+
78+
/**
79+
* @test
80+
*/
81+
public function TestInvalidTokenBody()
82+
{
83+
$mockGuzzle = new MockHandler([
84+
//Fetch token response
85+
new Response(200, ['Foo' => 'Bar']),
86+
// Fetch hostName response
87+
new Response(200, ['Foo' => 'Bar'], self::MOCK_HOSTNAME),
88+
// Fetch identities reponse
89+
new Response(200, ['Foo' => 'Bar'], self::MOCK_IDENTITY),
90+
]);
91+
92+
$handlerStack = HandlerStack::create($mockGuzzle);
93+
$client = new Client(['handler' => $handlerStack]);
94+
95+
$detector = new Ec2Detector($client);
96+
97+
$this->assertEquals(ResourceInfo::emptyResource(), $detector->detect());
98+
}
99+
100+
/**
101+
* @test
102+
*/
103+
public function TestInvalidTokenResponseCode()
104+
{
105+
$mockGuzzle = new MockHandler([
106+
//Fetch token response
107+
new Response(404, ['Foo' => 'Bar'], self::MOCK_TOKEN_RESPONSE),
108+
// Fetch hostName response
109+
new Response(200, ['Foo' => 'Bar'], self::MOCK_HOSTNAME),
110+
// Fetch identities reponse
111+
new Response(200, ['Foo' => 'Bar'], self::MOCK_IDENTITY),
112+
]);
113+
114+
$handlerStack = HandlerStack::create($mockGuzzle);
115+
$client = new Client(['handler' => $handlerStack]);
116+
117+
$detector = new Ec2Detector($client);
118+
119+
$this->assertEquals(ResourceInfo::emptyResource(), $detector->detect());
120+
}
121+
122+
/**
123+
* @test
124+
*/
125+
public function TestInvalidHostName()
126+
{
127+
$mockGuzzle = new MockHandler([
128+
//Fetch token response
129+
new Response(200, ['Foo' => 'Bar'], self::MOCK_TOKEN_RESPONSE),
130+
// Fetch hostName response
131+
new Response(200, ['Foo' => 'Bar']),
132+
// Fetch identities reponse
133+
new Response(200, ['Foo' => 'Bar'], self::MOCK_IDENTITY),
134+
]);
135+
136+
$handlerStack = HandlerStack::create($mockGuzzle);
137+
$client = new Client(['handler' => $handlerStack]);
138+
139+
$detector = new Ec2Detector($client);
140+
141+
$this->assertEquals(ResourceInfo::create(
142+
new Attributes(
143+
[
144+
ResourceConstants::HOST_ID => self::HOST_ID,
145+
ResourceConstants::CLOUD_ZONE => self::CLOUD_ZONE,
146+
ResourceConstants::HOST_TYPE => self::HOST_TYPE,
147+
ResourceConstants::HOST_IMAGE_ID => self::HOST_IMAGE_ID,
148+
ResourceConstants::CLOUD_ACCOUNT_ID => self::CLOUD_ACCOUNT_ID,
149+
ResourceConstants::CLOUD_REGION => self::CLOUD_REGION,
150+
ResourceConstants::CLOUD_PROVIDER => self::CLOUD_PROVIDER,
151+
]
152+
)
153+
), $detector->detect());
154+
}
155+
156+
/**
157+
* @test
158+
*/
159+
public function TestInvalidIdentities()
160+
{
161+
$mockGuzzle = new MockHandler([
162+
//Fetch token response
163+
new Response(200, ['Foo' => 'Bar'], self::MOCK_TOKEN_RESPONSE),
164+
// Fetch hostName response
165+
new Response(200, ['Foo' => 'Bar'], self::MOCK_HOSTNAME),
166+
// Fetch identities reponse
167+
new Response(200, ['Foo' => 'Bar']),
168+
]);
169+
170+
$handlerStack = HandlerStack::create($mockGuzzle);
171+
$client = new Client(['handler' => $handlerStack]);
172+
173+
$detector = new Ec2Detector($client);
174+
175+
$this->assertEquals(ResourceInfo::emptyResource(), $detector->detect());
176+
}
177+
178+
/**
179+
* @test
180+
*/
181+
public function TestInvalidIncompleteIdentities()
182+
{
183+
$mockGuzzle = new MockHandler([
184+
//Fetch token response
185+
new Response(200, ['Foo' => 'Bar'], self::MOCK_TOKEN_RESPONSE),
186+
// Fetch hostName response
187+
new Response(200, ['Foo' => 'Bar'], self::MOCK_HOSTNAME),
188+
// Fetch identities reponse
189+
new Response(200, ['Foo' => 'Bar'], self::MOCK_IDENTITY_INCOMPLETE),
190+
]);
191+
192+
$handlerStack = HandlerStack::create($mockGuzzle);
193+
$client = new Client(['handler' => $handlerStack]);
194+
195+
$detector = new Ec2Detector($client);
196+
197+
$this->assertEquals(ResourceInfo::create(
198+
new Attributes(
199+
[
200+
ResourceConstants::HOST_ID => self::HOST_ID,
201+
ResourceConstants::CLOUD_ZONE => self::CLOUD_ZONE,
202+
ResourceConstants::HOST_TYPE => self::HOST_TYPE,
203+
ResourceConstants::HOST_IMAGE_ID => self::HOST_IMAGE_ID,
204+
ResourceConstants::HOST_HOSTNAME => self::MOCK_HOSTNAME,
205+
ResourceConstants::CLOUD_PROVIDER => self::CLOUD_PROVIDER,
206+
]
207+
)
208+
), $detector->detect());
209+
}
210+
}

0 commit comments

Comments
 (0)