Skip to content

Commit 292d756

Browse files
committed
Slim: adding stable http server metrics
1 parent 1d5099d commit 292d756

File tree

5 files changed

+149
-13
lines changed

5 files changed

+149
-13
lines changed

src/Instrumentation/Slim/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"ext-opentelemetry": "*",
1414
"ext-reflection": "*",
1515
"open-telemetry/api": "^1.0",
16-
"open-telemetry/sem-conv": "^1.32",
16+
"open-telemetry/sem-conv": ">= 1.32.1",
1717
"slim/slim": "^4"
1818
},
1919
"require-dev": {

src/Instrumentation/Slim/phpstan.neon.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ parameters:
1010
excludePaths:
1111
analyseAndScan:
1212
- tests/Unit
13+
ignoreErrors:
14+
-
15+
message: "#Access to an undefined property .*#"
16+
paths:
17+
- tests/Integration/SlimInstrumentationTest.php
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Slim;
6+
7+
use OpenTelemetry\API\Metrics\HistogramInterface;
8+
use OpenTelemetry\API\Metrics\MeterInterface;
9+
use OpenTelemetry\SemConv\Metrics\HttpMetrics;
10+
use OpenTelemetry\SemConv\TraceAttributes;
11+
use Psr\Http\Message\ResponseInterface;
12+
use Psr\Http\Message\ServerRequestInterface;
13+
use Throwable;
14+
15+
class PsrServerRequestMetrics
16+
{
17+
private static ?HistogramInterface $serverRequestDuration;
18+
19+
/**
20+
* Generate HTTP Server Request metrics.
21+
* It implements the stable http.server.request.duration metric, along with required and recommended attributes as of SemConv 1.34.0
22+
*
23+
* @see https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration
24+
*/
25+
public static function generate(MeterInterface $meter, ServerRequestInterface $request, ?ResponseInterface $response, ?Throwable $exception): void
26+
{
27+
self::$serverRequestDuration ??= $meter->createHistogram(HttpMetrics::HTTP_SERVER_REQUEST_DURATION, 's', 'Duration of HTTP server requests');
28+
29+
$startTime = $request->getServerParams()['REQUEST_TIME_FLOAT'] ?? null;
30+
if ($startTime === null) {
31+
// without start time, we cannot measure the request duration
32+
return;
33+
}
34+
35+
if (self::$serverRequestDuration->isEnabled()) {
36+
$duration = (microtime(true) - (float) $startTime);
37+
$attributes = [
38+
TraceAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
39+
TraceAttributes::URL_SCHEME => $request->getUri()->getScheme(),
40+
];
41+
if ($response && $response->getStatusCode() >= 500) {
42+
//@see https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
43+
$attributes[TraceAttributes::EXCEPTION_TYPE] = (string) $response->getStatusCode();
44+
} elseif ($exception) {
45+
$attributes[TraceAttributes::EXCEPTION_TYPE] = $exception::class;
46+
}
47+
if ($response) {
48+
$attributes[TraceAttributes::HTTP_RESPONSE_BODY_SIZE] = (int) $response->getHeaderLine('Content-Length');
49+
$attributes[TraceAttributes::NETWORK_PROTOCOL_VERSION] = $response->getProtocolVersion();
50+
$attributes[TraceAttributes::HTTP_RESPONSE_STATUS_CODE] = $response->getStatusCode();
51+
}
52+
/** @psalm-suppress PossiblyInvalidArgument */
53+
self::$serverRequestDuration->record($duration, $attributes);
54+
}
55+
}
56+
57+
/**
58+
* @internal
59+
*/
60+
public static function reset(): void
61+
{
62+
self::$serverRequestDuration = null;
63+
}
64+
}

src/Instrumentation/Slim/src/SlimInstrumentation.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ public static function register(): void
7878

7979
return [$request];
8080
},
81-
post: static function (App $app, array $params, ?ResponseInterface $response, ?Throwable $exception): ?ResponseInterface {
81+
post: static function (App $app, array $params, ?ResponseInterface $response, ?Throwable $exception) use ($instrumentation): ?ResponseInterface {
82+
$request = ($params[0] instanceof ServerRequestInterface) ? $params[0] : null;
8283
$scope = Context::storage()->scope();
8384
if (!$scope) {
8485
return $response;
@@ -95,7 +96,7 @@ public static function register(): void
9596
}
9697
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());
9798
$span->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $response->getProtocolVersion());
98-
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, $response->getHeaderLine('Content-Length'));
99+
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, (int) $response->getHeaderLine('Content-Length'));
99100

100101
if (self::$supportsResponsePropagation) {
101102
// Propagate server-timing header to response, if ServerTimingPropagator is present
@@ -112,6 +113,9 @@ public static function register(): void
112113
}
113114
}
114115
$span->end();
116+
if ($request) {
117+
PsrServerRequestMetrics::generate($instrumentation->meter(), $request, $response, $exception);
118+
}
115119

116120
return $response;
117121
}

src/Instrumentation/Slim/tests/Integration/SlimInstrumentationTest.php

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@
1010
use Nyholm\Psr7\ServerRequest;
1111
use OpenTelemetry\API\Instrumentation\Configurator;
1212
use OpenTelemetry\Context\ScopeInterface;
13+
use OpenTelemetry\Contrib\Instrumentation\Slim\PsrServerRequestMetrics;
14+
use OpenTelemetry\SDK\Metrics\Data\HistogramDataPoint;
15+
use OpenTelemetry\SDK\Metrics\MeterProvider;
16+
use OpenTelemetry\SDK\Metrics\MetricExporter\InMemoryExporter as InMemoryMetricsExporter;
17+
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
1318
use OpenTelemetry\SDK\Trace\SpanExporter\InMemoryExporter;
1419
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
1520
use OpenTelemetry\SDK\Trace\TracerProvider;
21+
use OpenTelemetry\SemConv\TraceAttributes;
1622
use PHPUnit\Framework\TestCase;
1723
use Psr\Http\Message\ResponseInterface;
1824
use Psr\Http\Message\ServerRequestInterface;
@@ -28,25 +34,32 @@
2834
class SlimInstrumentationTest extends TestCase
2935
{
3036
private ScopeInterface $scope;
31-
private ArrayObject $storage;
37+
private ArrayObject $traces;
38+
private ArrayObject $metrics;
39+
private ExportingReader $reader;
3240

3341
public function setUp(): void
3442
{
35-
$this->storage = new ArrayObject();
43+
$this->traces = new ArrayObject();
44+
$this->metrics = new ArrayObject();
3645
$tracerProvider = new TracerProvider(
3746
new SimpleSpanProcessor(
38-
new InMemoryExporter($this->storage)
47+
new InMemoryExporter($this->traces)
3948
)
4049
);
50+
$this->reader = new ExportingReader(new InMemoryMetricsExporter($this->metrics));
51+
$meterProvider = MeterProvider::builder()->addReader($this->reader)->build();
4152

4253
$this->scope = Configurator::create()
4354
->withTracerProvider($tracerProvider)
55+
->withMeterProvider($meterProvider)
4456
->activate();
4557
}
4658

4759
public function tearDown(): void
4860
{
4961
$this->scope->detach();
62+
PsrServerRequestMetrics::reset();
5063
}
5164

5265
/**
@@ -67,8 +80,8 @@ public function performRouting(ServerRequestInterface $request): ServerRequestIn
6780
$routingMiddleware
6881
);
6982
$app->handle($request->withAttribute(RouteContext::ROUTE, $route));
70-
$this->assertCount(1, $this->storage);
71-
$span = $this->storage->offsetGet(0); // @var ImmutableSpan $span
83+
$this->assertCount(1, $this->traces);
84+
$span = $this->traces->offsetGet(0); // @var ImmutableSpan $span
7285
$this->assertSame($expected, $span->getName());
7386
}
7487

@@ -97,7 +110,7 @@ public function routeProvider(): array
97110
public function test_invocation_strategy(): void
98111
{
99112
$strategy = $this->createMockStrategy();
100-
$this->assertCount(0, $this->storage);
113+
$this->assertCount(0, $this->traces);
101114
$strategy->__invoke(
102115
function (): ResponseInterface {
103116
return new Response();
@@ -106,7 +119,7 @@ function (): ResponseInterface {
106119
$this->createMock(ResponseInterface::class),
107120
[]
108121
);
109-
$this->assertCount(1, $this->storage);
122+
$this->assertCount(1, $this->traces);
110123
}
111124

112125
public function test_routing_exception(): void
@@ -129,8 +142,8 @@ public function performRouting(ServerRequestInterface $request): ServerRequestIn
129142
} catch (\Exception $e) {
130143
$this->assertSame('routing failed', $e->getMessage());
131144
}
132-
$this->assertCount(1, $this->storage);
133-
$span = $this->storage->offsetGet(0); // @var ImmutableSpan $span
145+
$this->assertCount(1, $this->traces);
146+
$span = $this->traces->offsetGet(0); // @var ImmutableSpan $span
134147
$this->assertSame('GET', $span->getName(), 'span name was not updated because routing failed');
135148
}
136149

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

166+
/**
167+
* @psalm-suppress NoInterfaceProperties
168+
*/
169+
public function test_generate_metrics(): void
170+
{
171+
$request = (new ServerRequest(
172+
method: 'GET',
173+
uri: 'http://example.com/foo',
174+
serverParams: [
175+
'REQUEST_TIME_FLOAT' => microtime(true),
176+
],
177+
))->withHeader('Content-Length', '999');
178+
$route = Mockery::mock(RouteInterface::class)->allows([
179+
'getName' => 'route.name',
180+
'getPattern' => '/foo',
181+
]);
182+
183+
$routingMiddleware = new class($this->createMock(RouteResolverInterface::class), $this->createMock(RouteParserInterface::class)) extends RoutingMiddleware {
184+
public function performRouting(ServerRequestInterface $request): ServerRequestInterface
185+
{
186+
return $request;
187+
}
188+
};
189+
$app = $this->createMockApp(
190+
(new Response())->withHeader('Content-Length', '999'),
191+
$routingMiddleware
192+
);
193+
//execute twice to generate 2 data point values
194+
$app->handle($request->withAttribute(RouteContext::ROUTE, $route));
195+
$app->handle($request->withAttribute(RouteContext::ROUTE, $route));
196+
$this->assertCount(0, $this->metrics);
197+
$this->reader->collect();
198+
$this->assertCount(1, $this->metrics);
199+
$metric = $this->metrics->offsetGet(0);
200+
assert($metric instanceof \OpenTelemetry\SDK\Metrics\Data\Metric);
201+
$this->assertSame('http.server.request.duration', $metric->name);
202+
$this->assertCount(1, $metric->data->dataPoints);
203+
$dataPoint = $metric->data->dataPoints[0];
204+
assert($dataPoint instanceof HistogramDataPoint);
205+
$this->assertSame(2, $dataPoint->count);
206+
$attributes = $dataPoint->attributes->toArray();
207+
$this->assertEqualsCanonicalizing([
208+
TraceAttributes::HTTP_REQUEST_METHOD => 'GET',
209+
TraceAttributes::URL_SCHEME => 'http',
210+
TraceAttributes::HTTP_RESPONSE_BODY_SIZE => 999,
211+
TraceAttributes::NETWORK_PROTOCOL_VERSION => '1.1',
212+
TraceAttributes::HTTP_RESPONSE_STATUS_CODE => 200,
213+
], $attributes);
214+
}
215+
153216
public function createMockStrategy(): InvocationStrategyInterface
154217
{
155218
return new class() implements InvocationStrategyInterface {

0 commit comments

Comments
 (0)