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

Track http headers #13

Merged
merged 7 commits into from
Feb 12, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
options: --entrypoint redis-server

strategy:
fail-fast: true
fail-fast: false
matrix:
php: [ 8.1, 8.2, 8.3 ]
laravel: [ ^9.0, ^10.0 ]
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,16 @@ return [
Instrumentation\HttpServerInstrumentation::class => [
'enabled' => env('OT_INSTRUMENTATION_HTTP_SERVER', true),
'excluded_paths' => [],
'allowed_headers' => [],
'sensitive_headers' => [],
],

Instrumentation\HttpClientInstrumentation::class => [
'enabled' => env('OT_INSTRUMENTATION_HTTP_CLIENT', true),
'allowed_headers' => [],
'sensitive_headers' => [],
],

Instrumentation\HttpClientInstrumentation::class => env('OT_INSTRUMENTATION_HTTP_CLIENT', true),

Instrumentation\QueryInstrumentation::class => env('OT_INSTRUMENTATION_QUERY', true),

Instrumentation\RedisInstrumentation::class => env('OT_INSTRUMENTATION_REDIS', true),
Expand Down Expand Up @@ -129,6 +135,8 @@ You can disable it by setting `OT_INSTRUMENTATION_HTTP_SERVER` to `false` or rem
Configuration options:

- `excluded_paths`: list of paths to exclude from tracing
- `allowed_headers`: list of headers to include in the trace
- `sensitive_headers`: list of headers with sensitive data to hide in the trace

### Http client

Expand All @@ -138,6 +146,13 @@ To trace an outgoing http request call the `withTrace` method on the request bui
Http::withTrace()->get('https://example.com');
```

You can disable it by setting `OT_INSTRUMENTATION_HTTP_CLIENT` to `false` or removing the `HttpClientInstrumentation::class` from the config file.

Configuration options:

- `allowed_headers`: list of headers to include in the trace
- `sensitive_headers`: list of headers with sensitive data to hide in the trace

### Database

Database queries are automatically traced.
Expand Down
8 changes: 7 additions & 1 deletion config/opentelemetry.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@
Instrumentation\HttpServerInstrumentation::class => [
'enabled' => env('OT_INSTRUMENTATION_HTTP_SERVER', true),
'excluded_paths' => [],
'allowed_headers' => [],
'sensitive_headers' => [],
],

Instrumentation\HttpClientInstrumentation::class => env('OT_INSTRUMENTATION_HTTP_CLIENT', true),
Instrumentation\HttpClientInstrumentation::class => [
'enabled' => env('OT_INSTRUMENTATION_HTTP_CLIENT', true),
'allowed_headers' => [],
'sensitive_headers' => [],
],

Instrumentation\QueryInstrumentation::class => env('OT_INSTRUMENTATION_QUERY', true),

Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<php>
<env name="APP_KEY" value="base64:Otue13cqTvN3tEEu1F5C25ku031q5HktoEk2AnOZEgg="/>
</php>
</phpunit>
69 changes: 69 additions & 0 deletions src/Instrumentation/HandlesHttpHeaders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Keepsuit\LaravelOpenTelemetry\Instrumentation;

use Illuminate\Support\Arr;

/**
* @internal
*/
trait HandlesHttpHeaders
{
/**
* @var array<string>
*/
protected array $defaultSensitiveHeaders = [
'authorization',
'php-auth-pw',
'cookie',
'set-cookie',
];

/**
* @var array<string>
*/
protected static array $allowedHeaders = [];

/**
* @var array<string>
*/
protected static array $sensitiveHeaders = [];

/**
* @return array<string>
*/
public static function getAllowedHeaders(): array
{
return static::$allowedHeaders;
}

public static function headerIsAllowed(string $header): bool
{
return in_array($header, static::getAllowedHeaders());
}

/**
* @return array<string>
*/
public static function getSensitiveHeaders(): array
{
return static::$sensitiveHeaders;
}

public static function headerIsSensitive(string $header): bool
{
return in_array($header, static::getSensitiveHeaders());
}

/**
* @param array<string> $headers
* @return array<string>
*/
protected function normalizeHeaders(array $headers): array
{
return Arr::map(
$headers,
fn (string $header) => strtolower(trim($header)),
);
}
}
10 changes: 10 additions & 0 deletions src/Instrumentation/HttpClientInstrumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
namespace Keepsuit\LaravelOpenTelemetry\Instrumentation;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Arr;
use Keepsuit\LaravelOpenTelemetry\Support\HttpClient\GuzzleTraceMiddleware;

class HttpClientInstrumentation implements Instrumentation
{
use HandlesHttpHeaders;

public function register(array $options): void
{
static::$allowedHeaders = $this->normalizeHeaders(Arr::get($options, 'allowed_headers', []));

static::$sensitiveHeaders = array_merge(
$this->normalizeHeaders(Arr::get($options, 'sensitive_headers', [])),
$this->defaultSensitiveHeaders
);

$this->registerWithTraceMacro();
}

Expand Down
12 changes: 12 additions & 0 deletions src/Instrumentation/HttpServerInstrumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@

class HttpServerInstrumentation implements Instrumentation
{
use HandlesHttpHeaders;

protected static array $excludedPaths = [];

/**
* @return array<string>
*/
public static function getExcludedPaths(): array
{
return static::$excludedPaths;
Expand All @@ -22,6 +27,13 @@ public function register(array $options): void
Arr::get($options, 'excluded_paths', [])
);

static::$allowedHeaders = $this->normalizeHeaders(Arr::get($options, 'allowed_headers', []));

static::$sensitiveHeaders = array_merge(
$this->normalizeHeaders(Arr::get($options, 'sensitive_headers', [])),
$this->defaultSensitiveHeaders
);

$this->injectMiddleware(app(Kernel::class));
}

Expand Down
39 changes: 35 additions & 4 deletions src/Support/HttpClient/GuzzleTraceMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

use Closure;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Response;
use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
use Keepsuit\LaravelOpenTelemetry\Instrumentation\HttpClientInstrumentation;
use OpenTelemetry\API\Trace\SpanInterface;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\SemConv\TraceAttributes;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class GuzzleTraceMiddleware
{
Expand All @@ -29,6 +31,8 @@ public static function make(): Closure
->setAttribute(TraceAttributes::SERVER_PORT, $request->getUri()->getPort())
->start();

static::recordHeaders($span, $request);

$context = $span->storeInContext(Tracer::currentContext());

foreach (Tracer::propagationHeaders($context) as $key => $value) {
Expand All @@ -38,9 +42,14 @@ public static function make(): Closure
$promise = $handler($request, $options);
assert($promise instanceof PromiseInterface);

return $promise->then(function (Response $response) use ($span) {
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode())
->setAttribute(TraceAttributes::HTTP_REQUEST_BODY_SIZE, $response->getHeader('Content-Length')[0] ?? null);
return $promise->then(function (ResponseInterface $response) use ($span) {
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());

if (($contentLength = $response->getHeader('Content-Length')[0] ?? null) !== null) {
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, $contentLength);
}

static::recordHeaders($span, $response);

if ($response->getStatusCode() >= 400) {
$span->setStatus(StatusCode::STATUS_ERROR);
Expand All @@ -53,4 +62,26 @@ public static function make(): Closure
};
};
}

protected static function recordHeaders(SpanInterface $span, RequestInterface|ResponseInterface $http): SpanInterface
{
$prefix = match (true) {
$http instanceof RequestInterface => 'http.request.header.',
$http instanceof ResponseInterface => 'http.response.header.',
};

foreach ($http->getHeaders() as $key => $value) {
$key = strtolower($key);

if (! HttpClientInstrumentation::headerIsAllowed($key)) {
continue;
}

$value = HttpClientInstrumentation::headerIsSensitive($key) ? ['*****'] : $value;

$span->setAttribute($prefix.$key, $value);
}

return $span;
}
}
30 changes: 29 additions & 1 deletion src/Support/HttpServer/TraceRequestMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ protected function startTracing(Request $request): SpanInterface
$route = rescue(fn () => Route::getRoutes()->match($request)->uri(), $request->path(), false);
$route = str_starts_with($route, '/') ? $route : '/'.$route;

return Tracer::newSpan($route)
$span = Tracer::newSpan($route)
->setSpanKind(SpanKind::KIND_SERVER)
->setParent($context)
->setAttribute(TraceAttributes::URL_FULL, $request->fullUrl())
Expand All @@ -69,6 +69,10 @@ protected function startTracing(Request $request): SpanInterface
->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $request->getProtocolVersion())
->setAttribute(TraceAttributes::NETWORK_PEER_ADDRESS, $request->ip())
->start();

$this->recordHeaders($span, $request);

return $span;
}

protected function recordHttpResponseToSpan(SpanInterface $span, Response $response): void
Expand All @@ -79,6 +83,8 @@ protected function recordHttpResponseToSpan(SpanInterface $span, Response $respo
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, strlen($content));
}

$this->recordHeaders($span, $response);

if ($response->isSuccessful()) {
$span->setStatus(StatusCode::STATUS_OK);
}
Expand All @@ -87,4 +93,26 @@ protected function recordHttpResponseToSpan(SpanInterface $span, Response $respo
$span->setStatus(StatusCode::STATUS_ERROR);
}
}

protected function recordHeaders(SpanInterface $span, Request|Response $http): SpanInterface
{
$prefix = match (true) {
$http instanceof Request => 'http.request.header.',
$http instanceof Response => 'http.response.header.',
};

foreach ($http->headers->all() as $key => $value) {
$key = strtolower($key);

if (! HttpServerInstrumentation::headerIsAllowed($key)) {
continue;
}

$value = HttpServerInstrumentation::headerIsSensitive($key) ? ['*****'] : $value;

$span->setAttribute($prefix.$key, $value);
}

return $span;
}
}
Loading
Loading