Skip to content

Commit 673f2f9

Browse files
committed
[aws-sdk]Added Initial auto-instrumentation library for aws-sdk
1 parent 3835977 commit 673f2f9

File tree

11 files changed

+385
-0
lines changed

11 files changed

+385
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor/
2+
composer.lock
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
$finder = PhpCsFixer\Finder::create()
3+
->exclude('vendor')
4+
->exclude('tests/Unit81') //contains php8.1 syntax
5+
->exclude('var/cache')
6+
->exclude('tests/coverage')
7+
->in(__DIR__);
8+
9+
$config = new PhpCsFixer\Config();
10+
return $config->setRules([
11+
'concat_space' => ['spacing' => 'one'],
12+
'declare_equal_normalize' => ['space' => 'none'],
13+
'is_null' => true,
14+
'modernize_types_casting' => true,
15+
'ordered_imports' => true,
16+
'php_unit_construct' => true,
17+
'single_line_comment_style' => true,
18+
'yoda_style' => false,
19+
'@PSR2' => true,
20+
'array_syntax' => ['syntax' => 'short'],
21+
'blank_line_after_opening_tag' => true,
22+
'blank_line_before_statement' => true,
23+
'cast_spaces' => true,
24+
'declare_strict_types' => true,
25+
'type_declaration_spaces' => true,
26+
'include' => true,
27+
'lowercase_cast' => true,
28+
'new_with_parentheses' => true,
29+
'no_extra_blank_lines' => true,
30+
'no_leading_import_slash' => true,
31+
'echo_tag_syntax' => true,
32+
'no_unused_imports' => true,
33+
'no_useless_else' => true,
34+
'no_useless_return' => true,
35+
'phpdoc_order' => true,
36+
'phpdoc_scalar' => true,
37+
'phpdoc_types' => true,
38+
'short_scalar_cast' => true,
39+
'blank_lines_before_namespace' => true,
40+
'single_quote' => true,
41+
'trailing_comma_in_multiline' => true,
42+
])
43+
->setRiskyAllowed(true)
44+
->setFinder($finder);
45+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-auto-aws-sdk/releases)
2+
[![Issues](https://img.shields.io/badge/issues-pink)](https://github.com/open-telemetry/opentelemetry-php/issues)
3+
[![Source](https://img.shields.io/badge/source-contrib-green)](https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation/AwsSdk)
4+
[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/contrib-auto-aws-sdk)
5+
[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-auto-guzzle/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-aws-sdk/)
6+
[![Stable](http://poser.pugx.org/open-telemetry/opentelemetry-auto-aws-sdk/v/stable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-aws-sdk/)
7+
8+
This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib.
9+
10+
# OpenTelemetry AWS SDK auto-instrumentation
11+
Please read https://opentelemetry.io/docs/instrumentation/php/automatic/ for instructions on how to
12+
install and configure the extension and SDK.
13+
14+
## Overview
15+
Auto-instrumentation hooks are registered via composer.
16+
17+
* create spans automatically for each AWS SDK request that is sent
18+
19+
## Configuration
20+
21+
The extension can be disabled via [runtime configuration](https://opentelemetry.io/docs/instrumentation/php/sdk/#configuration):
22+
23+
```shell
24+
OTEL_PHP_DISABLED_INSTRUMENTATIONS=aws-sdk
25+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
use OpenTelemetry\Contrib\Instrumentation\AwsSdk\AwsSdkInstrumentation;
5+
use OpenTelemetry\SDK\Sdk;
6+
7+
if (class_exists(Sdk::class)
8+
&& Sdk::isInstrumentationDisabled(AwsSdkInstrumentation::NAME)) {
9+
return;
10+
}
11+
12+
if (!extension_loaded('opentelemetry')) {
13+
trigger_error(
14+
'The opentelemetry extension must be loaded to use the AWS SDK auto‑instrumentation',
15+
E_USER_WARNING
16+
);
17+
return;
18+
}
19+
20+
AwsSdkInstrumentation::register();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "open-telemetry/opentelemetry-auto-aws-sdk",
3+
"description": "OpenTelemetry auto‑instrumentation for the AWS SDK",
4+
"keywords": ["opentelemetry", "otel", "open-telemetry", "tracing", "aws-sdk", "instrumentation"],
5+
"type": "library",
6+
"homepage": "https://opentelemetry.io/docs/php",
7+
"readme": "./README.md",
8+
"license": "Apache-2.0",
9+
"minimum-stability": "dev",
10+
"prefer-stable": true,
11+
"require": {
12+
"php": "^8.2",
13+
"aws/aws-sdk-php": "^3",
14+
"ext-opentelemetry": "*",
15+
"open-telemetry/api": "^1.0"
16+
},
17+
"require-dev": {
18+
"friendsofphp/php-cs-fixer": "^3",
19+
"guzzlehttp/promises": "^2",
20+
"nyholm/psr7": "*",
21+
"phan/phan": "^5.0",
22+
"phpstan/phpstan-mockery": "^1.1.0",
23+
"phpstan/phpstan": "^1.1",
24+
"phpstan/phpstan-phpunit": "^1.0",
25+
"psalm/plugin-phpunit": "^0.19.2",
26+
"open-telemetry/sdk": "^1.0",
27+
"phpunit/phpunit": "^9.5",
28+
"vimeo/psalm": "6.4.0"
29+
},
30+
"autoload": {
31+
"psr-4": {
32+
"OpenTelemetry\\Contrib\\Instrumentation\\AwsSdk\\": "src/"
33+
},
34+
"files": ["_register.php"]
35+
},
36+
"autoload-dev": {
37+
"psr-4": {
38+
"OpenTelemetry\\Tests\\Instrumentation\\AwsSdk\\": "tests/"
39+
}
40+
},
41+
"config": {
42+
"allow-plugins": {
43+
"php-http/discovery": false,
44+
"tbachert/spi": true
45+
}
46+
}
47+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
includes:
2+
- vendor/phpstan/phpstan-phpunit/extension.neon
3+
4+
parameters:
5+
tmpDir: var/cache/phpstan
6+
level: 5
7+
paths:
8+
- src
9+
- tests
10+
excludePaths:
11+
analyseAndScan:
12+
- tests/Unit
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<phpunit
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
6+
backupGlobals="false"
7+
backupStaticAttributes="false"
8+
cacheResult="false"
9+
colors="false"
10+
convertErrorsToExceptions="true"
11+
convertNoticesToExceptions="true"
12+
convertWarningsToExceptions="true"
13+
forceCoversAnnotation="false"
14+
processIsolation="false"
15+
stopOnError="false"
16+
stopOnFailure="false"
17+
stopOnIncomplete="false"
18+
stopOnSkipped="false"
19+
stopOnRisky="false"
20+
timeoutForSmallTests="1"
21+
timeoutForMediumTests="10"
22+
timeoutForLargeTests="60"
23+
verbose="true">
24+
25+
<coverage processUncoveredFiles="true" disableCodeCoverageIgnore="false">
26+
<include>
27+
<directory>src</directory>
28+
</include>
29+
</coverage>
30+
31+
<php>
32+
<ini name="date.timezone" value="UTC" />
33+
<ini name="display_errors" value="On" />
34+
<ini name="display_startup_errors" value="On" />
35+
<ini name="error_reporting" value="E_ALL" />
36+
</php>
37+
38+
<testsuites>
39+
<testsuite name="unit">
40+
<directory>tests/Unit</directory>
41+
</testsuite>
42+
<testsuite name="integration">
43+
<directory>tests/Integration</directory>
44+
</testsuite>
45+
</testsuites>
46+
47+
</phpunit>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0"?>
2+
<psalm
3+
errorLevel="3"
4+
cacheDirectory="var/cache/psalm"
5+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
6+
xmlns="https://getpsalm.org/schema/config"
7+
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd">
8+
<projectFiles>
9+
<directory name="src"/>
10+
<directory name="tests"/>
11+
</projectFiles>
12+
<plugins>
13+
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
14+
</plugins>
15+
</psalm>
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
// src/Instrumentation/AwsSdk/AwsSdkInstrumentation.php
4+
declare(strict_types=1);
5+
6+
namespace OpenTelemetry\Contrib\Instrumentation\AwsSdk;
7+
8+
use Aws\AwsClient;
9+
use Aws\Exception\AwsException;
10+
use Aws\ResultInterface;
11+
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
12+
use OpenTelemetry\API\Trace\Span;
13+
use OpenTelemetry\API\Trace\SpanKind;
14+
use OpenTelemetry\API\Trace\StatusCode;
15+
use OpenTelemetry\Context\Context;
16+
use function OpenTelemetry\Instrumentation\hook;
17+
18+
final class AwsSdkInstrumentation
19+
{
20+
public const NAME = 'aws-sdk';
21+
22+
public static function register(): void
23+
{
24+
$inst = new CachedInstrumentation(
25+
'io.opentelemetry.contrib.php.aws-sdk',
26+
null,
27+
'https://opentelemetry.io/schemas/1.30.0',
28+
);
29+
30+
/**
31+
* ② Intercept the low‑level `execute` call that actually
32+
* performs the HTTP request and has the Command object.
33+
*/
34+
hook(
35+
AwsClient::class,
36+
'execute',
37+
pre: static function (
38+
AwsClient $c,
39+
array $params,
40+
string $class,
41+
string $func,
42+
?string $file,
43+
?int $line
44+
) use ($inst) {
45+
$cmd = $params[0];
46+
$builder = $inst->tracer()
47+
->spanBuilder("{$c->getApi()->getServiceName()}.{$cmd->getName()}")
48+
->setSpanKind(SpanKind::KIND_CLIENT)
49+
->setAttribute('rpc.system', 'aws-api')
50+
->setAttribute('rpc.method', $cmd->getName())
51+
->setAttribute('rpc.service', $c->getApi()->getServiceName())
52+
->setAttribute('aws.region', $c->getRegion())
53+
->setAttribute('code.function', $func)
54+
->setAttribute('code.namespace', $class)
55+
->setAttribute('code.filepath', $file)
56+
->setAttribute('code.line_number', $line);
57+
58+
$span = $builder->startSpan();
59+
Context::storage()->attach($span->storeInContext(Context::getCurrent()));
60+
},
61+
post: static function (
62+
AwsClient $c,
63+
array $params,
64+
mixed $result,
65+
?\Throwable $ex
66+
) {
67+
$scope = Context::storage()->scope();
68+
if (!$scope) {
69+
return;
70+
}
71+
$span = Span::fromContext($scope->context());
72+
$scope->detach();
73+
74+
if ($result instanceof ResultInterface && isset($result['@metadata'])) {
75+
$span->setAttribute('http.status_code', $result['@metadata']['statusCode']);
76+
$span->setAttribute('aws.requestId', $result['@metadata']['headers']['x-amz-request-id']);
77+
}
78+
if ($ex) {
79+
if ($ex instanceof AwsException && $ex->getAwsRequestId() !== null) {
80+
$span->setAttribute('aws.requestId', $ex->getAwsRequestId());
81+
}
82+
$span->recordException($ex);
83+
$span->setStatus(StatusCode::STATUS_ERROR, $ex->getMessage());
84+
}
85+
$span->end();
86+
}
87+
);
88+
}
89+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Tests\Contrib\Instrumentation\AwsSdk\Integration;
6+
7+
use ArrayObject;
8+
use Aws\MockHandler;
9+
use Aws\Result;
10+
use Aws\S3\S3Client;
11+
use OpenTelemetry\API\Instrumentation\Configurator;
12+
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
13+
use OpenTelemetry\Context\ScopeInterface;
14+
use OpenTelemetry\SDK\Trace\ImmutableSpan;
15+
use OpenTelemetry\SDK\Trace\SpanExporter\InMemoryExporter;
16+
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
17+
use OpenTelemetry\SDK\Trace\TracerProvider;
18+
use PHPUnit\Framework\TestCase;
19+
20+
/**
21+
* @covers \OpenTelemetry\Contrib\Instrumentation\AwsSdk\AwsSdkInstrumentation
22+
*/
23+
class AwsSdkInstrumentationTest extends TestCase
24+
{
25+
private S3Client $client;
26+
private MockHandler $mock;
27+
private ArrayObject $spans;
28+
private ScopeInterface $scope;
29+
30+
public function setUp(): void
31+
{
32+
$this->spans = new ArrayObject();
33+
$tracerProvider = new TracerProvider(
34+
new SimpleSpanProcessor(new InMemoryExporter($this->spans))
35+
);
36+
$this->scope = Configurator::create()
37+
->withTracerProvider($tracerProvider)
38+
->withPropagator(TraceContextPropagator::getInstance())
39+
->activate();
40+
41+
$this->mock = new MockHandler();
42+
$this->mock->append(new Result([
43+
'@metadata' => [
44+
'statusCode' => 200,
45+
'headers' => [
46+
'x-amz-request-id' => 'TEST-REQUEST-ID',
47+
],
48+
],
49+
]));
50+
51+
$this->client = new S3Client([
52+
'region' => 'us-west-2',
53+
'version' => 'latest',
54+
'handler' => $this->mock,
55+
]);
56+
}
57+
58+
public function tearDown(): void
59+
{
60+
$this->scope->detach();
61+
}
62+
63+
public function test_listBuckets_generates_one_aws_span_with_expected_attributes(): void
64+
{
65+
$this->client->listBuckets();
66+
67+
$this->assertCount(1, $this->spans);
68+
69+
$span = $this->spans->offsetGet(0);
70+
71+
$this->assertInstanceOf(ImmutableSpan::class, $span);
72+
73+
$this->assertSame('s3.ListBuckets', $span->getName());
74+
75+
$attrs = $span->getAttributes();
76+
$this->assertSame('aws-api', $attrs->get('rpc.system'));
77+
$this->assertSame('s3', $attrs->get('rpc.service'));
78+
$this->assertSame('ListBuckets', $attrs->get('rpc.method'));
79+
$this->assertSame('us-west-2', $attrs->get('aws.region'));
80+
$this->assertSame(200, $attrs->get('http.status_code'));
81+
$this->assertSame('TEST-REQUEST-ID', $attrs->get('aws.requestId'));
82+
}
83+
}

0 commit comments

Comments
 (0)