Skip to content

Commit 799985e

Browse files
intuibaseChrisLightfootWildbrettmc
authored
Curl auto instrumentation distributed tracing headers propagation, request and response headers capturing (#1420) (open-telemetry#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]>
1 parent 27a188b commit 799985e

File tree

6 files changed

+616
-84
lines changed

6 files changed

+616
-84
lines changed

src/Instrumentation/Curl/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,51 @@ install and configure the extension and SDK.
1414

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

1819
## Limitations
1920
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).
2021

22+
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.
23+
2124
## Configuration
2225

26+
### Disabling curl instrumentation
27+
2328
The extension can be disabled via [runtime configuration](https://opentelemetry.io/docs/instrumentation/php/sdk/#configuration):
2429

2530
```shell
2631
OTEL_PHP_DISABLED_INSTRUMENTATIONS=curl
2732
```
33+
34+
### Request and response headers capturing
35+
36+
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.
37+
38+
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:
39+
40+
#### Environment variables configuration
41+
42+
```bash
43+
OTEL_PHP_INSTRUMENTATION_HTTP_RESPONSE_HEADERS=content-type,server
44+
OTEL_PHP_INSTRUMENTATION_HTTP_REQUEST_HEADERS=host,accept
45+
```
46+
47+
#### php.ini configuration
48+
49+
```ini
50+
OTEL_PHP_INSTRUMENTATION_HTTP_RESPONSE_HEADERS=content-type,server
51+
; or
52+
otel.instrumentation.http.response_headers[]=content-type
53+
otel.instrumentation.http.response_headers[]=server
54+
```
55+
56+
57+
Similarly, to capture headers sent in a request to the server, use the following configuration:
58+
59+
```ini
60+
OTEL_PHP_INSTRUMENTATION_HTTP_REQUEST_HEADERS=host,accept
61+
; or
62+
otel.instrumentation.http.request_headers[]=host
63+
otel.instrumentation.http.request_headers[]=accept
64+
```
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Curl;
6+
7+
use CurlHandle;
8+
use OpenTelemetry\SemConv\TraceAttributes;
9+
10+
class CurlHandleMetadata
11+
{
12+
private array $attributes = [];
13+
14+
private array $headers = [];
15+
16+
private array $headersToPropagate = [];
17+
18+
private mixed $originalHeaderFunction = null;
19+
private array $responseHeaders = [];
20+
21+
private bool $verboseEnabled = false;
22+
23+
public function __construct()
24+
{
25+
$this->attributes = [TraceAttributes::HTTP_REQUEST_METHOD => 'GET'];
26+
$this->headers = [];
27+
$headersToPropagate = [];
28+
}
29+
30+
public function isVerboseEnabled(): bool
31+
{
32+
return $this->verboseEnabled;
33+
}
34+
35+
public function getAttributes(): array
36+
{
37+
return $this->attributes;
38+
}
39+
40+
public function setAttribute(string $key, mixed $value)
41+
{
42+
$this->attributes[$key] = $value;
43+
}
44+
45+
public function setHeaderToPropagate(string $key, $value): CurlHandleMetadata
46+
{
47+
$this->headersToPropagate[] = $key . ': ' . $value;
48+
49+
return $this;
50+
}
51+
52+
public function getRequestHeadersToSend(): ?array
53+
{
54+
if (count($this->headersToPropagate) == 0) {
55+
return null;
56+
}
57+
$headers = array_merge($this->headersToPropagate, $this->headers);
58+
$this->headersToPropagate = [];
59+
60+
return $headers;
61+
}
62+
63+
public function getCapturedResponseHeaders(): array
64+
{
65+
return $this->responseHeaders;
66+
}
67+
68+
public function getResponseHeaderCaptureFunction()
69+
{
70+
$this->responseHeaders = [];
71+
$func = function (CurlHandle $handle, string $headerLine): int {
72+
$header = trim($headerLine, "\n\r");
73+
74+
if (strlen($header) > 0) {
75+
if (strpos($header, ': ') !== false) {
76+
/** @psalm-suppress PossiblyUndefinedArrayOffset */
77+
[$key, $value] = explode(': ', $header, 2);
78+
$this->responseHeaders[strtolower($key)] = $value;
79+
}
80+
}
81+
82+
if ($this->originalHeaderFunction) {
83+
return call_user_func($this->originalHeaderFunction, $handle, $headerLine);
84+
}
85+
86+
return strlen($headerLine);
87+
};
88+
89+
return \Closure::bind($func, $this, self::class);
90+
}
91+
92+
public function updateFromCurlOption(int $option, mixed $value)
93+
{
94+
switch ($option) {
95+
case CURLOPT_CUSTOMREQUEST:
96+
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $value);
97+
98+
break;
99+
case CURLOPT_HTTPGET:
100+
// Based on https://github.com/curl/curl/blob/curl-7_73_0/lib/setopt.c#L841
101+
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, 'GET');
102+
103+
break;
104+
case CURLOPT_POST:
105+
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, ($value == 1 ? 'POST' : 'GET'));
106+
107+
break;
108+
case CURLOPT_POSTFIELDS:
109+
// Based on https://github.com/curl/curl/blob/curl-7_73_0/lib/setopt.c#L269
110+
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, 'POST');
111+
112+
break;
113+
case CURLOPT_PUT:
114+
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, ($value == 1 ? 'PUT' : 'GET'));
115+
116+
break;
117+
case CURLOPT_NOBODY:
118+
// Based on https://github.com/curl/curl/blob/curl-7_73_0/lib/setopt.c#L269
119+
$this->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, ($value == 1 ? 'HEAD' : 'GET'));
120+
121+
break;
122+
case CURLOPT_URL:
123+
$this->setAttribute(TraceAttributes::URL_FULL, self::redactUrlString($value));
124+
125+
break;
126+
case CURLOPT_USERAGENT:
127+
$this->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $value);
128+
129+
break;
130+
case CURLOPT_HTTPHEADER:
131+
$this->headers = $value;
132+
133+
break;
134+
case CURLOPT_HEADERFUNCTION:
135+
$this->originalHeaderFunction = $value;
136+
$this->verboseEnabled = false;
137+
138+
break;
139+
case CURLOPT_VERBOSE:
140+
$this->verboseEnabled = $value;
141+
142+
break;
143+
}
144+
}
145+
146+
public static function redactUrlString(string $fullUrl)
147+
{
148+
$urlParts = parse_url($fullUrl);
149+
if ($urlParts == false) {
150+
return;
151+
}
152+
153+
$scheme = isset($urlParts['scheme']) ? $urlParts['scheme'] . '://' : '';
154+
$host = isset($urlParts['host']) ? $urlParts['host'] : '';
155+
$port = isset($urlParts['port']) ? ':' . $urlParts['port'] : '';
156+
$user = isset($urlParts['user']) ? 'REDACTED' : '';
157+
$pass = isset($urlParts['pass']) ? ':' . 'REDACTED' : '';
158+
$pass = ($user || $pass) ? "$pass@" : '';
159+
$path = isset($urlParts['path']) ? $urlParts['path'] : '';
160+
$query = isset($urlParts['query']) ? '?' . $urlParts['query'] : '';
161+
$fragment = isset($urlParts['fragment']) ? '#' . $urlParts['fragment'] : '';
162+
163+
return $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
164+
}
165+
166+
}

0 commit comments

Comments
 (0)