Skip to content

Commit

Permalink
track http headers (client)
Browse files Browse the repository at this point in the history
  • Loading branch information
cappuc committed Feb 12, 2024
1 parent d3f1a9a commit b444e47
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 40 deletions.
6 changes: 5 additions & 1 deletion config/opentelemetry.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
'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
59 changes: 59 additions & 0 deletions src/Instrumentation/HandlesHttpHeaders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?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;
}

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

/**
* @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
40 changes: 5 additions & 35 deletions src/Instrumentation/HttpServerInstrumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,10 @@

class HttpServerInstrumentation implements Instrumentation
{
protected const DEFAULT_SENSITIVE_HEADERS = [
'authorization',
'php-auth-pw',
'cookie',
'set-cookie',
];
use HandlesHttpHeaders;

protected static array $excludedPaths = [];

protected static array $allowedHeaders = [];

protected static array $sensitiveHeaders = [];

/**
* @return array<string>
*/
Expand All @@ -29,39 +20,18 @@ public static function getExcludedPaths(): array
return static::$excludedPaths;
}

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

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

public function register(array $options): void
{
static::$excludedPaths = array_map(
fn (string $path) => ltrim($path, '/'),
Arr::get($options, 'excluded_paths', [])
);

static::$allowedHeaders = array_map(
fn (string $header) => strtolower(trim($header)),
Arr::get($options, 'allowed_headers', [])
);
static::$allowedHeaders = $this->normalizeHeaders(Arr::get($options, 'allowed_headers', []));

static::$sensitiveHeaders = array_merge(
array_map(
fn (string $header) => strtolower(trim($header)),
Arr::get($options, 'sensitive_headers', [])
),
self::DEFAULT_SENSITIVE_HEADERS
$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 (! in_array($key, HttpClientInstrumentation::getAllowedHeaders())) {
continue;
}

$value = in_array($key, HttpClientInstrumentation::getSensitiveHeaders()) ? ['*****'] : $value;

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

return $span;
}
}
2 changes: 2 additions & 0 deletions src/Support/HttpServer/TraceRequestMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ protected function recordHeaders(SpanInterface $span, Request|Response $http): S
};

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

if (! in_array($key, HttpServerInstrumentation::getAllowedHeaders())) {
continue;
}
Expand Down
100 changes: 100 additions & 0 deletions tests/Instrumentation/HttpClientInstrumentationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
use Keepsuit\LaravelOpenTelemetry\Instrumentation\HttpClientInstrumentation;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;

Expand Down Expand Up @@ -89,3 +90,102 @@
'http.response.status_code' => 500,
]);
});

it('trace allowed request headers', function () {
app()->make(HttpClientInstrumentation::class)->register([
'allowed_headers' => [
'x-foo',
],
]);

Server::enqueue([
new Response(200, ['Content-Length' => 0]),
]);

Http::withHeaders([
'x-foo' => 'bar',
'x-bar' => 'baz',
])->withTrace()->get(Server::$url);

$span = getRecordedSpans()[0];

expect($span->getAttributes())
->toMatchArray([
'http.request.header.x-foo' => ['bar'],
])
->not->toHaveKey('http.request.header.x-bar');
});

it('trace allowed response headers', function () {
app()->make(HttpClientInstrumentation::class)->register([
'allowed_headers' => [
'content-type',
],
]);

Server::enqueue([
new Response(200, ['Content-Length' => 0, 'Content-Type' => 'text/html; charset=UTF-8']),
]);

Http::withTrace()->get(Server::$url);

$span = getRecordedSpans()[0];

expect($span->getAttributes())
->toMatchArray([
'http.response.header.content-type' => ['text/html; charset=UTF-8'],
])
->not->toHaveKey('http.response.header.date');
});

it('trace sensitive headers with hidden value', function () {
app()->make(HttpClientInstrumentation::class)->register([
'allowed_headers' => [
'x-foo',
],
'sensitive_headers' => [
'x-foo',
],
]);

Server::enqueue([
new Response(200, ['Content-Length' => 0]),
]);

Http::withHeaders(['x-foo' => 'bar'])->withTrace()->get(Server::$url);

$span = getRecordedSpans()[0];

expect($span->getAttributes())
->toMatchArray([
'http.request.header.x-foo' => ['*****'],
]);
});

it('mark some headers as sensitive by default', function () {
app()->make(HttpClientInstrumentation::class)->register([
'allowed_headers' => [
'authorization',
'cookie',
'set-cookie',
],
]);

Server::enqueue([
new Response(200, ['Content-Length' => 0, 'Set-Cookie' => 'cookie']),
]);

Http::withHeaders([
'authorization' => 'Bearer token',
'cookie' => 'cookie',
])->withTrace()->get(Server::$url);

$span = getRecordedSpans()[0];

expect($span->getAttributes())
->toMatchArray([
'http.request.header.authorization' => ['*****'],
'http.request.header.cookie' => ['*****'],
'http.response.header.set-cookie' => ['*****'],
]);
});

0 comments on commit b444e47

Please sign in to comment.