Skip to content
Closed
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 src/Instrumentation/Slim/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"ext-opentelemetry": "*",
"ext-reflection": "*",
"open-telemetry/api": "^1.0",
"open-telemetry/sem-conv": "^1.32",
"open-telemetry/sem-conv": ">= 1.32.1",
"slim/slim": "^4"
},
"require-dev": {
Expand Down
5 changes: 5 additions & 0 deletions src/Instrumentation/Slim/phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ parameters:
excludePaths:
analyseAndScan:
- tests/Unit
ignoreErrors:
-
message: "#Access to an undefined property .*#"
paths:
- tests/Integration/SlimInstrumentationTest.php
64 changes: 64 additions & 0 deletions src/Instrumentation/Slim/src/PsrServerRequestMetrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Slim;

use OpenTelemetry\API\Metrics\HistogramInterface;
use OpenTelemetry\API\Metrics\MeterInterface;
use OpenTelemetry\SemConv\Metrics\HttpMetrics;
use OpenTelemetry\SemConv\TraceAttributes;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;

class PsrServerRequestMetrics
{
private static ?HistogramInterface $serverRequestDuration;

/**
* Generate HTTP Server Request metrics.
* It implements the stable http.server.request.duration metric, along with required and recommended attributes as of SemConv 1.34.0
*
* @see https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration
*/
public static function generate(MeterInterface $meter, ServerRequestInterface $request, ?ResponseInterface $response, ?Throwable $exception): void
{
self::$serverRequestDuration ??= $meter->createHistogram(HttpMetrics::HTTP_SERVER_REQUEST_DURATION, 's', 'Duration of HTTP server requests');

$startTime = $request->getServerParams()['REQUEST_TIME_FLOAT'] ?? null;
if ($startTime === null) {
// without start time, we cannot measure the request duration
return;
}

if (self::$serverRequestDuration->isEnabled()) {
$duration = (microtime(true) - (float) $startTime);
$attributes = [
TraceAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
TraceAttributes::URL_SCHEME => $request->getUri()->getScheme(),
];
if ($response && $response->getStatusCode() >= 500) {
//@see https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
$attributes[TraceAttributes::EXCEPTION_TYPE] = (string) $response->getStatusCode();
} elseif ($exception) {
$attributes[TraceAttributes::EXCEPTION_TYPE] = $exception::class;
}
if ($response) {
$attributes[TraceAttributes::HTTP_RESPONSE_BODY_SIZE] = (int) $response->getHeaderLine('Content-Length');
$attributes[TraceAttributes::NETWORK_PROTOCOL_VERSION] = $response->getProtocolVersion();
$attributes[TraceAttributes::HTTP_RESPONSE_STATUS_CODE] = $response->getStatusCode();
}
/** @psalm-suppress PossiblyInvalidArgument */
self::$serverRequestDuration->record($duration, $attributes);
}
}

/**
* @internal
*/
public static function reset(): void
{
self::$serverRequestDuration = null;
}
}
8 changes: 6 additions & 2 deletions src/Instrumentation/Slim/src/SlimInstrumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ public static function register(): void

return [$request];
},
post: static function (App $app, array $params, ?ResponseInterface $response, ?Throwable $exception): ?ResponseInterface {
post: static function (App $app, array $params, ?ResponseInterface $response, ?Throwable $exception) use ($instrumentation): ?ResponseInterface {
$request = ($params[0] instanceof ServerRequestInterface) ? $params[0] : null;
$scope = Context::storage()->scope();
if (!$scope) {
return $response;
Expand All @@ -95,7 +96,7 @@ public static function register(): void
}
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());
$span->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $response->getProtocolVersion());
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, $response->getHeaderLine('Content-Length'));
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, (int) $response->getHeaderLine('Content-Length'));

if (self::$supportsResponsePropagation) {
// Propagate server-timing header to response, if ServerTimingPropagator is present
Expand All @@ -112,6 +113,9 @@ public static function register(): void
}
}
$span->end();
if ($request) {
PsrServerRequestMetrics::generate($instrumentation->meter(), $request, $response, $exception);
}

return $response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@
use Nyholm\Psr7\ServerRequest;
use OpenTelemetry\API\Instrumentation\Configurator;
use OpenTelemetry\Context\ScopeInterface;
use OpenTelemetry\Contrib\Instrumentation\Slim\PsrServerRequestMetrics;
use OpenTelemetry\SDK\Metrics\Data\HistogramDataPoint;
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MetricExporter\InMemoryExporter as InMemoryMetricsExporter;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\SDK\Trace\SpanExporter\InMemoryExporter;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\TraceAttributes;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand All @@ -28,25 +34,32 @@
class SlimInstrumentationTest extends TestCase
{
private ScopeInterface $scope;
private ArrayObject $storage;
private ArrayObject $traces;
private ArrayObject $metrics;
private ExportingReader $reader;

public function setUp(): void
{
$this->storage = new ArrayObject();
$this->traces = new ArrayObject();
$this->metrics = new ArrayObject();
$tracerProvider = new TracerProvider(
new SimpleSpanProcessor(
new InMemoryExporter($this->storage)
new InMemoryExporter($this->traces)
)
);
$this->reader = new ExportingReader(new InMemoryMetricsExporter($this->metrics));
$meterProvider = MeterProvider::builder()->addReader($this->reader)->build();

$this->scope = Configurator::create()
->withTracerProvider($tracerProvider)
->withMeterProvider($meterProvider)
->activate();
}

public function tearDown(): void
{
$this->scope->detach();
PsrServerRequestMetrics::reset();
}

/**
Expand All @@ -67,8 +80,8 @@ public function performRouting(ServerRequestInterface $request): ServerRequestIn
$routingMiddleware
);
$app->handle($request->withAttribute(RouteContext::ROUTE, $route));
$this->assertCount(1, $this->storage);
$span = $this->storage->offsetGet(0); // @var ImmutableSpan $span
$this->assertCount(1, $this->traces);
$span = $this->traces->offsetGet(0); // @var ImmutableSpan $span
$this->assertSame($expected, $span->getName());
}

Expand Down Expand Up @@ -97,7 +110,7 @@ public function routeProvider(): array
public function test_invocation_strategy(): void
{
$strategy = $this->createMockStrategy();
$this->assertCount(0, $this->storage);
$this->assertCount(0, $this->traces);
$strategy->__invoke(
function (): ResponseInterface {
return new Response();
Expand All @@ -106,7 +119,7 @@ function (): ResponseInterface {
$this->createMock(ResponseInterface::class),
[]
);
$this->assertCount(1, $this->storage);
$this->assertCount(1, $this->traces);
}

public function test_routing_exception(): void
Expand All @@ -129,8 +142,8 @@ public function performRouting(ServerRequestInterface $request): ServerRequestIn
} catch (\Exception $e) {
$this->assertSame('routing failed', $e->getMessage());
}
$this->assertCount(1, $this->storage);
$span = $this->storage->offsetGet(0); // @var ImmutableSpan $span
$this->assertCount(1, $this->traces);
$span = $this->traces->offsetGet(0); // @var ImmutableSpan $span
$this->assertSame('GET', $span->getName(), 'span name was not updated because routing failed');
}

Expand All @@ -143,13 +156,63 @@ public function test_response_propagation(): void
$request = (new ServerRequest('GET', 'https://example.com/foo'));
$app = $this->createMockApp(new Response(200, ['X-Foo' => 'foo']));
$response = $app->handle($request);
$this->assertCount(1, $this->storage);
$this->assertCount(1, $this->traces);
$this->assertArrayHasKey('X-Foo', $response->getHeaders());
$this->assertArrayHasKey('server-timing', $response->getHeaders());
$this->assertStringStartsWith('traceparent;desc=', $response->getHeaderLine('server-timing'));
$this->assertArrayHasKey('traceresponse', $response->getHeaders());
}

/**
* @psalm-suppress NoInterfaceProperties
*/
public function test_generate_metrics(): void
{
$request = (new ServerRequest(
method: 'GET',
uri: 'http://example.com/foo',
serverParams: [
'REQUEST_TIME_FLOAT' => microtime(true),
],
))->withHeader('Content-Length', '999');
$route = Mockery::mock(RouteInterface::class)->allows([
'getName' => 'route.name',
'getPattern' => '/foo',
]);

$routingMiddleware = new class($this->createMock(RouteResolverInterface::class), $this->createMock(RouteParserInterface::class)) extends RoutingMiddleware {
public function performRouting(ServerRequestInterface $request): ServerRequestInterface
{
return $request;
}
};
$app = $this->createMockApp(
(new Response())->withHeader('Content-Length', '999'),
$routingMiddleware
);
//execute twice to generate 2 data point values
$app->handle($request->withAttribute(RouteContext::ROUTE, $route));
$app->handle($request->withAttribute(RouteContext::ROUTE, $route));
$this->assertCount(0, $this->metrics);
$this->reader->collect();
$this->assertCount(1, $this->metrics);
$metric = $this->metrics->offsetGet(0);
assert($metric instanceof \OpenTelemetry\SDK\Metrics\Data\Metric);
$this->assertSame('http.server.request.duration', $metric->name);
$this->assertCount(1, $metric->data->dataPoints);
$dataPoint = $metric->data->dataPoints[0];
assert($dataPoint instanceof HistogramDataPoint);
$this->assertSame(2, $dataPoint->count);
$attributes = $dataPoint->attributes->toArray();
$this->assertEqualsCanonicalizing([
TraceAttributes::HTTP_REQUEST_METHOD => 'GET',
TraceAttributes::URL_SCHEME => 'http',
TraceAttributes::HTTP_RESPONSE_BODY_SIZE => 999,
TraceAttributes::NETWORK_PROTOCOL_VERSION => '1.1',
TraceAttributes::HTTP_RESPONSE_STATUS_CODE => 200,
], $attributes);
}

public function createMockStrategy(): InvocationStrategyInterface
{
return new class() implements InvocationStrategyInterface {
Expand Down