Skip to content

Commit

Permalink
guzzle auto-instrumentation (#226)
Browse files Browse the repository at this point in the history
* guzzle auto-instrumentation
* apply review feedback
* update semconv
* adding guzzle to ci and gitsplit
* update readme
  • Loading branch information
brettmc authored Jan 11, 2024
0 parents commit 385a9e5
Show file tree
Hide file tree
Showing 14 changed files with 915 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
* text=auto

*.md diff=markdown
*.php diff=php

/.gitattributes export-ignore
/.gitignore export-ignore
/.phan export-ignore
/.php-cs-fixer.php export-ignore
/phpstan.neon.dist export-ignore
/phpunit.xml.dist export-ignore
/psalm.xml.dist export-ignore
/tests export-ignore
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vendor/
371 changes: 371 additions & 0 deletions .phan/config.php

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
$finder = PhpCsFixer\Finder::create()
->exclude('vendor')
->exclude('tests/Unit81') //contains php8.1 syntax
->exclude('var/cache')
->exclude('tests/coverage')
->in(__DIR__);

$config = new PhpCsFixer\Config();
return $config->setRules([
'concat_space' => ['spacing' => 'one'],
'declare_equal_normalize' => ['space' => 'none'],
'is_null' => true,
'modernize_types_casting' => true,
'ordered_imports' => true,
'php_unit_construct' => true,
'single_line_comment_style' => true,
'yoda_style' => false,
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'blank_line_after_opening_tag' => true,
'blank_line_before_statement' => true,
'cast_spaces' => true,
'declare_strict_types' => true,
'type_declaration_spaces' => true,
'include' => true,
'lowercase_cast' => true,
'new_with_parentheses' => true,
'no_extra_blank_lines' => true,
'no_leading_import_slash' => true,
'echo_tag_syntax' => true,
'no_unused_imports' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'phpdoc_order' => true,
'phpdoc_scalar' => true,
'phpdoc_types' => true,
'short_scalar_cast' => true,
'blank_lines_before_namespace' => true,
'single_quote' => true,
'trailing_comma_in_multiline' => true,
])
->setRiskyAllowed(true)
->setFinder($finder);

26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-auto-guzzle/releases)
[![Issues](https://img.shields.io/badge/issues-pink)](https://github.com/open-telemetry/opentelemetry-php/issues)
[![Source](https://img.shields.io/badge/source-contrib-green)](https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation/Guzzle)
[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/contrib-auto-guzzle)
[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-auto-guzzle/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-guzzle/)
[![Stable](http://poser.pugx.org/open-telemetry/opentelemetry-auto-guzzle/v/stable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-guzzle/)

This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib.

# OpenTelemetry Guzzle auto-instrumentation
Please read https://opentelemetry.io/docs/instrumentation/php/automatic/ for instructions on how to
install and configure the extension and SDK.

## Overview
Auto-instrumentation hooks are registered via composer.

* create spans automatically for each Guzzle request that is sent (sync or async)
* add a `traceparent` header to the request to facilitate distributed tracing

## Configuration

The extension can be disabled via [runtime configuration](https://opentelemetry.io/docs/instrumentation/php/sdk/#configuration):

```shell
OTEL_PHP_DISABLED_INSTRUMENTATIONS=guzzle
```
18 changes: 18 additions & 0 deletions _register.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

use OpenTelemetry\Contrib\Instrumentation\Guzzle\GuzzleInstrumentation;
use OpenTelemetry\SDK\Sdk;

if (class_exists(Sdk::class) && Sdk::isInstrumentationDisabled(GuzzleInstrumentation::NAME) === true) {
return;
}

if (extension_loaded('opentelemetry') === false) {
trigger_error('The opentelemetry extension must be loaded in order to autoload the OpenTelemetry Guzzle auto-instrumentation', E_USER_WARNING);

return;
}

GuzzleInstrumentation::register();
49 changes: 49 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "open-telemetry/opentelemetry-auto-guzzle",
"description": "OpenTelemetry auto-instrumentation for Guzzle.",
"keywords": ["opentelemetry", "otel", "open-telemetry", "tracing", "guzzle", "instrumentation"],
"type": "library",
"homepage": "https://opentelemetry.io/docs/php",
"readme": "./README.md",
"license": "Apache-2.0",
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": "^8.0",
"ext-opentelemetry": "*",
"open-telemetry/api": "^1.0",
"open-telemetry/sem-conv": "^1.23",
"guzzlehttp/guzzle": "^7"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3",
"guzzlehttp/promises": "^2",
"nyholm/psr7": "*",
"phan/phan": "^5.0",
"phpstan/phpstan-mockery": "^1.1.0",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"psalm/plugin-phpunit": "^0.16",
"open-telemetry/sdk": "^1.0",
"phpunit/phpunit": "^9.5",
"vimeo/psalm": "^4.0"
},
"autoload": {
"psr-4": {
"OpenTelemetry\\Contrib\\Instrumentation\\Guzzle\\": "src/"
},
"files": [
"_register.php"
]
},
"autoload-dev": {
"psr-4": {
"OpenTelemetry\\Tests\\Instrumentation\\Guzzle\\": "tests/"
}
},
"config": {
"allow-plugins": {
"php-http/discovery": false
}
}
}
12 changes: 12 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon

parameters:
tmpDir: var/cache/phpstan
level: 5
paths:
- src
- tests
excludePaths:
analyseAndScan:
- tests/Unit
47 changes: 47 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>

<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
backupGlobals="false"
backupStaticAttributes="false"
cacheResult="false"
colors="false"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
stopOnRisky="false"
timeoutForSmallTests="1"
timeoutForMediumTests="10"
timeoutForLargeTests="60"
verbose="true">

<coverage processUncoveredFiles="true" disableCodeCoverageIgnore="false">
<include>
<directory>src</directory>
</include>
</coverage>

<php>
<ini name="date.timezone" value="UTC" />
<ini name="display_errors" value="On" />
<ini name="display_startup_errors" value="On" />
<ini name="error_reporting" value="E_ALL" />
</php>

<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>

</phpunit>
15 changes: 15 additions & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<psalm
errorLevel="3"
cacheDirectory="var/cache/psalm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd">
<projectFiles>
<directory name="src"/>
<directory name="tests"/>
</projectFiles>
<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
</psalm>
121 changes: 121 additions & 0 deletions src/GuzzleInstrumentation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Guzzle;

use function get_cfg_var;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\PromiseInterface;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use function OpenTelemetry\Instrumentation\hook;
use OpenTelemetry\SemConv\TraceAttributes;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use function sprintf;
use function strtolower;
use Throwable;

class GuzzleInstrumentation
{
/** @psalm-suppress ArgumentTypeCoercion */
public const NAME = 'guzzle';

public static function register(): void
{
$instrumentation = new CachedInstrumentation('io.opentelemetry.contrib.php.guzzle', schemaUrl: TraceAttributes::SCHEMA_URL);

hook(
ClientInterface::class,
'transfer',
pre: static function (ClientInterface $client, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation): array {
$request = $params[0];
assert($request instanceof RequestInterface);

$propagator = Globals::propagator();
$parentContext = Context::getCurrent();

/** @psalm-suppress ArgumentTypeCoercion */
$spanBuilder = $instrumentation
->tracer()
->spanBuilder(sprintf('%s', $request->getMethod()))
->setParent($parentContext)
->setSpanKind(SpanKind::KIND_CLIENT)
->setAttribute(TraceAttributes::URL_FULL, (string) $request->getUri())
->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $request->getMethod())
->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $request->getProtocolVersion())
->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $request->getHeaderLine('User-Agent'))
->setAttribute(TraceAttributes::HTTP_REQUEST_BODY_SIZE, $request->getHeaderLine('Content-Length'))
->setAttribute(TraceAttributes::SERVER_ADDRESS, $request->getUri()->getHost())
->setAttribute(TraceAttributes::SERVER_PORT, $request->getUri()->getPort())
->setAttribute(TraceAttributes::URL_PATH, $request->getUri()->getPath())
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
->setAttribute(TraceAttributes::CODE_LINENO, $lineno)
;

foreach ($propagator->fields() as $field) {
$request = $request->withoutHeader($field);
}
foreach ((array) (get_cfg_var('otel.instrumentation.http.request_headers') ?: []) as $header) {
if ($request->hasHeader($header)) {
$spanBuilder->setAttribute(
sprintf('http.request.header.%s', strtolower($header)),
$request->getHeader($header)
);
}
}

$span = $spanBuilder->startSpan();
$context = $span->storeInContext($parentContext);
$propagator->inject($request, HeadersPropagator::instance(), $context);

Context::storage()->attach($context);

return [$request];
},
post: static function (ClientInterface $client, array $params, PromiseInterface $promise, ?Throwable $exception): void {
$scope = Context::storage()->scope();
$scope?->detach();

if (!$scope || $scope->context() === Context::getCurrent()) {
return;
}

$span = Span::fromContext($scope->context());
if ($exception) {
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
$span->end();
}

$promise->then(
onFulfilled: function (ResponseInterface $response) use ($span) {
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());
$span->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $response->getProtocolVersion());
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, $response->getHeaderLine('Content-Length'));
if ($response->getStatusCode() >= 400 && $response->getStatusCode() < 600) {
$span->setStatus(StatusCode::STATUS_ERROR);
}
$span->end();

return $response;
},
onRejected: function (\Throwable $t) use ($span) {
$span->recordException($t, [TraceAttributes::EXCEPTION_ESCAPED => true]);
$span->setStatus(StatusCode::STATUS_ERROR, $t->getMessage());
$span->end();

throw $t;
}
);
}
);
}
}
29 changes: 29 additions & 0 deletions src/HeadersPropagator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Guzzle;

use function assert;
use OpenTelemetry\Context\Propagation\PropagationSetterInterface;
use Psr\Http\Message\RequestInterface;

/**
* @internal
*/
class HeadersPropagator implements PropagationSetterInterface
{
public static function instance(): self
{
static $instance;

return $instance ??= new self();
}

public function set(&$carrier, string $key, string $value): void
{
assert($carrier instanceof RequestInterface);

$carrier = $carrier->withAddedHeader($key, $value);
}
}
Loading

0 comments on commit 385a9e5

Please sign in to comment.