Skip to content

Commit

Permalink
Curl auto instrumentation distributed tracing headers propagation, re…
Browse files Browse the repository at this point in the history
…quest and response headers capturing (#1420) (#314)

* Curl auto instrumentation distributed tracing headers propagation, request and response headers capturing (#1420)

* Apply suggestions from code review

Co-authored-by: Chris Lightfoot-Wild <[email protected]>

* Fixes after code review

* Update src/Instrumentation/Curl/tests/Integration/CurlInstrumentationTest.php

Co-authored-by: Brett McBride <[email protected]>

* Update src/Instrumentation/Curl/tests/Integration/CurlMultiInstrumentationTest.php

Co-authored-by: Brett McBride <[email protected]>

---------

Co-authored-by: Chris Lightfoot-Wild <[email protected]>
Co-authored-by: Brett McBride <[email protected]>
  • Loading branch information
3 people authored Nov 6, 2024
1 parent 27a188b commit 799985e
Show file tree
Hide file tree
Showing 6 changed files with 616 additions and 84 deletions.
37 changes: 37 additions & 0 deletions src/Instrumentation/Curl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,51 @@ install and configure the extension and SDK.

## Overview
Auto-instrumentation hooks are registered via composer, and client kind spans will automatically be created when calling `curl_exec` or `curl_multi_exec` functions.
Additionally, distributed tracing is supported by setting the `traceparent` header.

## Limitations
The curl_multi instrumentation is not resilient to shortcomings in the application and requires proper implementation. If the application does not call the curl_multi_info_read function, the instrumentation will be unable to measure the execution time for individual requests-time will be aggregated for all transfers. Similarly, error detection will be impacted, as the error code information will be missing in this case. In case of encountered issues, it is recommended to review the application code and adjust it to match example #1 provided in [curl_multi_exec documentation](https://www.php.net/manual/en/function.curl-multi-exec.php).

To ensure the stability of the monitored application, capturing request headers sent to the server works only if the application does not use the `CURLOPT_VERBOSE` option.

## Configuration

### Disabling curl instrumentation

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

```shell
OTEL_PHP_DISABLED_INSTRUMENTATIONS=curl
```

### Request and response headers capturing

Curl auto-instrumentation enables capturing headers from both requests and responses. This feature is disabled by default and be enabled through environment variables or array directives in the `php.ini` configuration file.

To enable response header capture from the server, specify the required headers as shown in the example below. In this case, the "Content-Type" and "Server" headers will be captured. These options values are case-insensitive:

#### Environment variables configuration

```bash
OTEL_PHP_INSTRUMENTATION_HTTP_RESPONSE_HEADERS=content-type,server
OTEL_PHP_INSTRUMENTATION_HTTP_REQUEST_HEADERS=host,accept
```

#### php.ini configuration

```ini
OTEL_PHP_INSTRUMENTATION_HTTP_RESPONSE_HEADERS=content-type,server
; or
otel.instrumentation.http.response_headers[]=content-type
otel.instrumentation.http.response_headers[]=server
```


Similarly, to capture headers sent in a request to the server, use the following configuration:

```ini
OTEL_PHP_INSTRUMENTATION_HTTP_REQUEST_HEADERS=host,accept
; or
otel.instrumentation.http.request_headers[]=host
otel.instrumentation.http.request_headers[]=accept
```
166 changes: 166 additions & 0 deletions src/Instrumentation/Curl/src/CurlHandleMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Curl;

use CurlHandle;
use OpenTelemetry\SemConv\TraceAttributes;

class CurlHandleMetadata
{
private array $attributes = [];

private array $headers = [];

private array $headersToPropagate = [];

private mixed $originalHeaderFunction = null;
private array $responseHeaders = [];

private bool $verboseEnabled = false;

public function __construct()
{
$this->attributes = [TraceAttributes::HTTP_REQUEST_METHOD => 'GET'];
$this->headers = [];
$headersToPropagate = [];
}

public function isVerboseEnabled(): bool
{
return $this->verboseEnabled;
}

public function getAttributes(): array
{
return $this->attributes;
}

public function setAttribute(string $key, mixed $value)
{
$this->attributes[$key] = $value;
}

public function setHeaderToPropagate(string $key, $value): CurlHandleMetadata
{
$this->headersToPropagate[] = $key . ': ' . $value;

return $this;
}

public function getRequestHeadersToSend(): ?array
{
if (count($this->headersToPropagate) == 0) {
return null;
}
$headers = array_merge($this->headersToPropagate, $this->headers);
$this->headersToPropagate = [];

return $headers;
}

public function getCapturedResponseHeaders(): array
{
return $this->responseHeaders;
}

public function getResponseHeaderCaptureFunction()
{
$this->responseHeaders = [];
$func = function (CurlHandle $handle, string $headerLine): int {
$header = trim($headerLine, "\n\r");

if (strlen($header) > 0) {
if (strpos($header, ': ') !== false) {
/** @psalm-suppress PossiblyUndefinedArrayOffset */
[$key, $value] = explode(': ', $header, 2);
$this->responseHeaders[strtolower($key)] = $value;
}
}

if ($this->originalHeaderFunction) {
return call_user_func($this->originalHeaderFunction, $handle, $headerLine);
}

return strlen($headerLine);
};

return \Closure::bind($func, $this, self::class);
}

public function updateFromCurlOption(int $option, mixed $value)
{
switch ($option) {
case CURLOPT_CUSTOMREQUEST:
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $value);

break;
case CURLOPT_HTTPGET:
// Based on https://github.com/curl/curl/blob/curl-7_73_0/lib/setopt.c#L841
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, 'GET');

break;
case CURLOPT_POST:
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, ($value == 1 ? 'POST' : 'GET'));

break;
case CURLOPT_POSTFIELDS:
// Based on https://github.com/curl/curl/blob/curl-7_73_0/lib/setopt.c#L269
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, 'POST');

break;
case CURLOPT_PUT:
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, ($value == 1 ? 'PUT' : 'GET'));

break;
case CURLOPT_NOBODY:
// Based on https://github.com/curl/curl/blob/curl-7_73_0/lib/setopt.c#L269
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, ($value == 1 ? 'HEAD' : 'GET'));

break;
case CURLOPT_URL:
$this->setAttribute(TraceAttributes::URL_FULL, self::redactUrlString($value));

break;
case CURLOPT_USERAGENT:
$this->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $value);

break;
case CURLOPT_HTTPHEADER:
$this->headers = $value;

break;
case CURLOPT_HEADERFUNCTION:
$this->originalHeaderFunction = $value;
$this->verboseEnabled = false;

break;
case CURLOPT_VERBOSE:
$this->verboseEnabled = $value;

break;
}
}

public static function redactUrlString(string $fullUrl)
{
$urlParts = parse_url($fullUrl);
if ($urlParts == false) {
return;
}

$scheme = isset($urlParts['scheme']) ? $urlParts['scheme'] . '://' : '';
$host = isset($urlParts['host']) ? $urlParts['host'] : '';
$port = isset($urlParts['port']) ? ':' . $urlParts['port'] : '';
$user = isset($urlParts['user']) ? 'REDACTED' : '';
$pass = isset($urlParts['pass']) ? ':' . 'REDACTED' : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($urlParts['path']) ? $urlParts['path'] : '';
$query = isset($urlParts['query']) ? '?' . $urlParts['query'] : '';
$fragment = isset($urlParts['fragment']) ? '#' . $urlParts['fragment'] : '';

return $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
}

}
Loading

0 comments on commit 799985e

Please sign in to comment.