Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading