Skip to content

Commit

Permalink
feat: implement cloud{provider,platform}, aws.{ecs.*,log.*} in ECS de…
Browse files Browse the repository at this point in the history
…tector (#83)

* feat: implement cloud.provider, cloud.platform, aws.ecs, aws.log in ECS Detector

* Document in comments what the detector does

* Implement logging and add a test for metadata endpoint failure
  • Loading branch information
Michele Mancioppi authored Nov 2, 2022
1 parent 23464b2 commit 824c0b7
Show file tree
Hide file tree
Showing 8 changed files with 762 additions and 41 deletions.
161 changes: 144 additions & 17 deletions src/Aws/src/Ecs/Detector.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@

namespace OpenTelemetry\Aws\Ecs;

use OpenTelemetry\SDK\Behavior\LogsMessagesTrait;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SemConv\ResourceAttributes;
use OpenTelemetry\SemConv\ResourceAttributeValues;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Throwable;

/**
Expand All @@ -33,40 +37,76 @@
*/
class Detector implements ResourceDetectorInterface
{
use LogsMessagesTrait;

private const ECS_METADATA_KEY_V4 = 'ECS_CONTAINER_METADATA_URI_V4';
private const ECS_METADATA_KEY_V3 = 'ECS_CONTAINER_METADATA_URI';

private const CONTAINER_ID_LENGTH = 64;

private DataProvider $processData;
private ClientInterface $client;
private RequestFactoryInterface $requestFactory;

public function __construct(DataProvider $processData)
{
public function __construct(
DataProvider $processData,
ClientInterface $client,
RequestFactoryInterface $requestFactory
) {
$this->processData = $processData;
$this->client = $client;
$this->requestFactory = $requestFactory;
}

/**
* If running on ECS, runs getContainerId(), getClusterName(), and
* returns resource with valid extracted values
* If not running on ECS, returns empty rsource
* If not running on ECS, returns empty resource.
*
* If running on ECS with an ECS agent v1.3, returns a resource with the following attributes set:
* - cloud.provider => aws
* - cloud.platform => aws_ecs
* - container.name => <hostname>, which is usually the container name in the ECS task definition
* - container.id => <cgroup_id>
*
* If running on ECS with an ECS agent v1.4, the returned resource has additionally the following
* attributes as specified in https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud_provider/aws/ecs/:
*
* - aws.ecs.container.arn
* - aws.ecs.cluster.arn
* - aws.ecs.launchtype
* - aws.ecs.task.arn
* - aws.ecs.task.family
* - aws.ecs.task.revision
*
* If running on ECS with an ECS agent v1.4 and the task definition is configured to report
* logs in AWS CloudWatch, the returned resource has additionally the following attributes as specified
* in https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud_provider/aws/logs:
*
* - aws.log.group.names
* - aws.log.group.arns
* - aws.log.stream.names
* - aws.log.stream.arns
*/
public function getResource(): ResourceInfo
{
// Check if running on ECS by looking for below environment variables
if (!getenv(self::ECS_METADATA_KEY_V4) && !getenv(self::ECS_METADATA_KEY_V3)) {
// TODO: add 'Process is not running on ECS' when logs are added
$metadataEndpointV4 = getenv(self::ECS_METADATA_KEY_V4);

if (!$metadataEndpointV4 && !getenv(self::ECS_METADATA_KEY_V3)) {
return ResourceInfoFactory::emptyResource();
}

$hostName = $this->processData->getHostname();
$containerId = $this->getContainerId();
$basicEcsResource = ResourceInfo::create(Attributes::create([
ResourceAttributes::CLOUD_PROVIDER => ResourceAttributeValues::CLOUD_PROVIDER_AWS,
ResourceAttributes::CLOUD_PLATFORM => ResourceAttributeValues::CLOUD_PLATFORM_AWS_ECS,
]));

return !$hostName && !$containerId
? ResourceInfoFactory::emptyResource()
: ResourceInfo::create(Attributes::create([
ResourceAttributes::CONTAINER_NAME => $hostName,
ResourceAttributes::CONTAINER_ID => $containerId,
]));
$metadataV4Resource = $this->getMetadataEndpointV4Resource();

$hostNameAndContainerIdResource = ResourceInfo::create(Attributes::create([
ResourceAttributes::CONTAINER_NAME => $this->processData->getHostname(),
ResourceAttributes::CONTAINER_ID => $this->getContainerId(),
]));

return ResourceInfoFactory::merge($basicEcsResource, $hostNameAndContainerIdResource, $metadataV4Resource);
}

/**
Expand All @@ -88,9 +128,96 @@ private function getContainerId(): ?string
}
}
} catch (Throwable $e) {
//TODO: add 'Failed to read container ID' when logging is added
self::logDebug('Failed to read container ID', ['exception' => $e]);
}

return null;
}

private function getMetadataEndpointV4Resource(): ResourceInfo
{
$metadataEndpointV4 = getenv(self::ECS_METADATA_KEY_V4);
if (!$metadataEndpointV4) {
return ResourceInfoFactory::emptyResource();
}

$containerRequest = $this->requestFactory
->createRequest('GET', $metadataEndpointV4);
$containerResponse = $this->client->sendRequest($containerRequest);
if ($containerResponse->getStatusCode() > 299) {
self::logError(sprintf('Cannot retrieve container metadata from %s endpoint', $metadataEndpointV4), [
'status_code' => $containerResponse->getStatusCode(),
'response_body' => $containerResponse->getBody()->getContents(),
]);

return ResourceInfoFactory::emptyResource();
}

$taskRequest = $this->requestFactory
->createRequest('GET', $metadataEndpointV4 . '/task');
$taskResponse = $this->client->sendRequest($taskRequest);
if ($taskResponse->getStatusCode() > 299) {
self::logError(sprintf('Cannot retrieve task metadata from %s endpoint', $metadataEndpointV4 . '/task'), [
'status_code' => $taskResponse->getStatusCode(),
'response_body' => $taskResponse->getBody()->getContents(),
]);

return ResourceInfoFactory::emptyResource();
}

$containerMetadata = json_decode($containerResponse->getBody()->getContents(), true);
$taskMetadata = json_decode($taskResponse->getBody()->getContents(), true);

$launchType = isset($taskMetadata['LaunchType']) ? strtolower($taskMetadata['LaunchType']) : null;
$taskFamily = isset($taskMetadata['Family']) ? $taskMetadata['Family'] : null;
$taskRevision = isset($taskMetadata['Revision']) ? $taskMetadata['Revision'] : null;

$clusterArn = null;
$taskArn = null;
if (isset($taskMetadata['Cluster']) && isset($taskMetadata['TaskARN'])) {
$taskArn = $taskMetadata['TaskARN'];
$lastIndexOfColon = strrpos($taskArn, ':');
if ($lastIndexOfColon) {
$baseArn = substr($taskArn, 0, $lastIndexOfColon);
$cluster = $taskMetadata['Cluster'];
$clusterArn = strpos($cluster, 'arn:') === 0 ? $cluster : $baseArn . ':cluster/' . $cluster;
}
}

$containerArn = isset($containerMetadata['ContainerARN']) ? $containerMetadata['ContainerARN'] : null;

$logResource = ResourceInfoFactory::emptyResource();
if (isset($containerMetadata['LogOptions']) && isset($containerMetadata['LogDriver']) && $containerMetadata['LogDriver'] === 'awslogs') {
$logOptions = $containerMetadata['LogOptions'];
$logsGroupName = $logOptions['awslogs-group'];
$logsStreamName = $logOptions['awslogs-stream'];

$logsGroupArns = [];
$logsStreamArns = [];
if (isset($containerMetadata['ContainerARN']) && preg_match('/arn:aws:ecs:([^:]+):([^:]+):.*/', $containerMetadata['ContainerARN'], $matches)) {
[$arn, $awsRegion, $awsAccount] = $matches;

$logsGroupArns = ['arn:aws:logs:' . $awsRegion . ':' . $awsAccount . ':log-group:' . $logsGroupName];
$logsStreamArns = ['arn:aws:logs:' . $awsRegion . ':' . $awsAccount . ':log-group:' . $logsGroupName . ':log-stream:' . $logsStreamName];
}

$logResource = ResourceInfo::create(Attributes::create([
ResourceAttributes::AWS_LOG_GROUP_NAMES => [$logsGroupName],
ResourceAttributes::AWS_LOG_GROUP_ARNS => $logsGroupArns,
ResourceAttributes::AWS_LOG_STREAM_NAMES => [$logsStreamName],
ResourceAttributes::AWS_LOG_STREAM_ARNS => $logsStreamArns,
]));
}

$ecsResource = ResourceInfo::create(Attributes::create([
ResourceAttributes::AWS_ECS_CONTAINER_ARN => $containerArn,
ResourceAttributes::AWS_ECS_CLUSTER_ARN => $clusterArn,
ResourceAttributes::AWS_ECS_LAUNCHTYPE => $launchType,
ResourceAttributes::AWS_ECS_TASK_ARN => $taskArn,
ResourceAttributes::AWS_ECS_TASK_FAMILY => $taskFamily,
ResourceAttributes::AWS_ECS_TASK_REVISION => $taskRevision,
]));

return ResourceInfoFactory::merge($ecsResource, $logResource);
}
}
Loading

0 comments on commit 824c0b7

Please # to comment.