Skip to content

Commit 475ed08

Browse files
feat: add uri_mask support (#10)
1 parent 395826c commit 475ed08

File tree

7 files changed

+214
-8
lines changed

7 files changed

+214
-8
lines changed

publish/open-telemetry.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
'export_interval' => (int) env('OTEL_TRACES_EXPORT_INTERVAL', 5),
3636
'processor' => env('OTEL_TRACES_PROCESSOR', 'batch'),
3737
'sampler' => env('OTEL_TRACES_SAMPLER', 'always_on'),
38+
'uri_mask' => [],
3839
'exporters' => [
3940
'otlp_http' => [
4041
'driver' => OtlpHttpTraceExporterFactory::class,
@@ -80,6 +81,7 @@
8081
'enabled' => env('OTEL_METRICS_ENABLED', true),
8182
'exporter' => env('OTEL_METRICS_EXPORTER', 'otlp_http'),
8283
'export_interval' => (int) env('OTEL_METRICS_EXPORT_INTERVAL', 5),
84+
'uri_mask' => [],
8385
'exporters' => [
8486
'otlp_http' => [
8587
'driver' => OtlpHttpMetricExporterFactory::class,

src/Aspect/GuzzleClientAspect.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint): mixed
6565

6666
if ($this->isTracingEnabled) {
6767
$scope = $this->instrumentation->startSpan(
68-
name: $method . ' ' . Uri::sanitize($request->getUri()->getPath()),
68+
name: $method . ' ' . Uri::sanitize(
69+
$request->getUri()->getPath(),
70+
$this->config->get('open-telemetry.traces.uri_mask', [])
71+
),
6972
spanKind: SpanKind::KIND_CLIENT,
7073
attributes: [
7174
HttpAttributes::HTTP_REQUEST_METHOD => $method,
@@ -128,9 +131,12 @@ private function onFullFilled(?SpanScope $scope, RequestInterface $request, floa
128131
->createHistogram('http.client.request.duration', 'ms')
129132
->record($duration, [
130133
ServerAttributes::SERVER_ADDRESS => $request->getUri()->getHost(),
131-
UrlIncubatingAttributes::URL_TEMPLATE => Uri::sanitize($request->getUri()->getPath()),
132134
HttpAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
133135
HttpAttributes::HTTP_RESPONSE_STATUS_CODE => $response->getStatusCode(),
136+
UrlIncubatingAttributes::URL_TEMPLATE => Uri::sanitize(
137+
$request->getUri()->getPath(),
138+
$this->config->get('open-telemetry.metrics.uri_mask', []),
139+
),
134140
]);
135141
}
136142

@@ -156,8 +162,11 @@ private function onRejected(?SpanScope $scope, RequestInterface $request, float
156162
ServerAttributes::SERVER_ADDRESS => $request->getUri()->getHost(),
157163
HttpAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
158164
HttpAttributes::HTTP_RESPONSE_STATUS_CODE => 0,
159-
UrlIncubatingAttributes::URL_TEMPLATE => Uri::sanitize($request->getUri()->getPath()),
160165
ErrorAttributes::ERROR_TYPE => get_class($throwable),
166+
UrlIncubatingAttributes::URL_TEMPLATE => Uri::sanitize(
167+
$request->getUri()->getPath(),
168+
$this->config->get('open-telemetry.metrics.uri_mask', []),
169+
),
161170
]
162171
);
163172
}

src/Middleware/MetricMiddleware.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
3030
$startTime = microtime(true);
3131

3232
$attributes = [
33-
HttpAttributes::HTTP_ROUTE => Uri::sanitize($request->getUri()->getPath()),
3433
HttpAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
34+
HttpAttributes::HTTP_ROUTE => Uri::sanitize(
35+
$request->getUri()->getPath(),
36+
$this->config->get('open-telemetry.metrics.uri_mask', [])
37+
),
3538
];
3639

3740
try {

src/Middleware/TraceMiddleware.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
3737
}
3838

3939
$context = $this->instrumentation->propagator()->extract($request->getHeaders());
40-
$name = Uri::sanitize($path);
4140

4241
$scope = $this->instrumentation->startSpan(
43-
name: $name,
42+
name: Uri::sanitize($path, $this->config->get('open-telemetry.traces.uri_mask', [])),
4443
spanKind: SpanKind::KIND_SERVER,
4544
attributes: [
4645
HttpAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),

tests/Unit/Aspect/GuzzleClientAspectTest.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ protected function setUp(): void
8686
['open-telemetry.metrics.exporters.otlp_http.options.endpoint', 'localhost', 'http://collector:4318'],
8787
['open-telemetry.instrumentation.features.guzzle.options.headers.request', ['*'], ['*']],
8888
['open-telemetry.instrumentation.features.guzzle.options.headers.response', ['*'], ['*']],
89+
['open-telemetry.traces.uri_mask', [], ['/P2P[0-9A-Za-z]+/' => '{identifier}']],
90+
['open-telemetry.metrics.uri_mask', [], ['/P2P[0-9A-Za-z]+/' => '{identifier}']],
8991
]);
9092

9193
$this->proceedingJoinPoint->arguments = ['keys' => ['request' => $this->request]];
@@ -215,6 +217,95 @@ public function testProcessWithSuccessRequest(): void
215217

216218
$this->assertEquals($this->promise, $result);
217219
}
220+
public function testProcessWithUriMaskAndSuccessRequest(): void
221+
{
222+
$this->configureRequestMock(
223+
'GET',
224+
'https://api.example.com/v1/users/P2P123/transactions?page=1',
225+
['User-Agent' => ['TestAgent/1.0']]
226+
);
227+
228+
$this->configureResponseMock();
229+
230+
$aspect = new GuzzleClientAspect(
231+
$this->config,
232+
$this->instrumentation,
233+
$this->switcher
234+
);
235+
236+
$this->promise->expects($this->once())
237+
->method('then')
238+
->willReturnCallback(function ($fullFilled, $rejected) {
239+
$this->assertIsCallable($fullFilled);
240+
$this->assertIsCallable($rejected);
241+
242+
$fullFilled($this->response);
243+
244+
return $this->promise;
245+
});
246+
247+
// Span
248+
$this->instrumentation
249+
->expects($this->once())
250+
->method('startSpan')
251+
->with(
252+
$this->equalTo('GET /v1/users/{identifier}/transactions'),
253+
$this->equalTo(SpanKind::KIND_CLIENT),
254+
[
255+
HttpAttributes::HTTP_REQUEST_METHOD => 'GET',
256+
UrlAttributes::URL_FULL => 'https://api.example.com/v1/users/P2P123/transactions?page=1',
257+
UrlAttributes::URL_PATH => '/v1/users/P2P123/transactions',
258+
UrlAttributes::URL_SCHEME => 'https',
259+
UrlAttributes::URL_QUERY => 'page=1',
260+
ServerAttributes::SERVER_ADDRESS => 'api.example.com',
261+
ServerAttributes::SERVER_PORT => 443,
262+
UserAgentAttributes::USER_AGENT_ORIGINAL => 'TestAgent/1.0',
263+
'http.request.header.user-agent' => 'TestAgent/1.0',
264+
]
265+
)
266+
->willReturn($this->spanScope);
267+
268+
$this->propagator->expects($this->once())
269+
->method('inject')
270+
->with($this->request, $this->anything(), $this->spanScope->getContext());
271+
272+
$this->spanScope->expects($this->once())->method('detach');
273+
274+
$this->spanScope->expects($this->never())->method('setStatus');
275+
276+
$this->spanScope->expects($this->once())
277+
->method('setAttributes')
278+
->with([
279+
HttpAttributes::HTTP_RESPONSE_STATUS_CODE => 200,
280+
HttpIncubatingAttributes::HTTP_RESPONSE_BODY_SIZE => '1024',
281+
'http.response.header.content-type' => 'application/json',
282+
'http.response.header.content-length' => '1024',
283+
]);
284+
285+
$this->spanScope->expects($this->once())->method('end');
286+
287+
// Metric
288+
$this->meter->expects($this->once())
289+
->method('createHistogram')
290+
->with('http.client.request.duration', 'ms')
291+
->willReturn($this->histogram);
292+
293+
$this->histogram->expects($this->once())
294+
->method('record')
295+
->with(
296+
$this->isType('float'),
297+
[
298+
ServerAttributes::SERVER_ADDRESS => 'api.example.com',
299+
UrlIncubatingAttributes::URL_TEMPLATE => '/v1/users/{identifier}/transactions',
300+
HttpAttributes::HTTP_REQUEST_METHOD => 'GET',
301+
HttpAttributes::HTTP_RESPONSE_STATUS_CODE => 200,
302+
]
303+
);
304+
305+
$result = $aspect->process($this->proceedingJoinPoint);
306+
307+
$this->assertEquals($this->promise, $result);
308+
}
218309

219310
public function testProcessWithBadRequest(): void
220311
{

tests/Unit/Middleware/MetricMiddlewareTest.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ protected function setUp(): void
6060
$this->switcher->method('isMetricsEnabled')->willReturn(true);
6161

6262
$this->config->method('get')
63-
->with('open-telemetry.instrumentation.features.client_request.options.ignore_paths', [])
64-
->willReturn([]);
63+
->willReturnMap([
64+
['open-telemetry.instrumentation.features.client_request.options.ignore_paths', [], []],
65+
['open-telemetry.instrumentation.features.client_request.options.headers.response', ['*'], ['*']],
66+
['open-telemetry.traces.uri_mask', [], ['/P2P[0-9A-Za-z]+/' => '{identifier}']],
67+
['open-telemetry.metrics.uri_mask', [], ['/P2P[0-9A-Za-z]+/' => '{identifier}']],
68+
]);
6569

6670
$this->request->method('getUri')->willReturn($this->uri);
6771
$this->instrumentation->method('meter')->willReturn($this->meter);
@@ -104,6 +108,39 @@ public function testProcessWithSuccessfulRequest(): void
104108
$this->assertSame($this->response, $result);
105109
}
106110

111+
public function testProcessWithUriMaskAndSuccessRequest(): void
112+
{
113+
$this->configureRequestMock('GET', '/users/P2P123');
114+
$this->configureResponseMock(200);
115+
116+
$this->meter->method('createHistogram')
117+
->with(HttpMetrics::HTTP_SERVER_REQUEST_DURATION, 'ms')
118+
->willReturn($this->histogram);
119+
120+
$expectedAttributes = [
121+
HttpAttributes::HTTP_ROUTE => '/users/{identifier}',
122+
HttpAttributes::HTTP_REQUEST_METHOD => 'GET',
123+
HttpAttributes::HTTP_RESPONSE_STATUS_CODE => 200,
124+
];
125+
126+
$this->histogram->expects($this->once())
127+
->method('record')
128+
->with(
129+
$this->greaterThan(0),
130+
$expectedAttributes
131+
);
132+
133+
$middleware = new MetricMiddleware(
134+
$this->config,
135+
$this->instrumentation,
136+
$this->switcher
137+
);
138+
139+
$result = $middleware->process($this->request, $this->handler);
140+
141+
$this->assertSame($this->response, $result);
142+
}
143+
107144
public function testProcessWithIgnoredPath(): void
108145
{
109146
$this->configureRequestMock('GET', '/health');

tests/Unit/Middleware/TraceMiddlewareTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ protected function setUp(): void
7373
->willReturnMap([
7474
['open-telemetry.instrumentation.features.client_request.options.ignore_paths', [], []],
7575
['open-telemetry.instrumentation.features.client_request.options.headers.response', ['*'], ['*']],
76+
['open-telemetry.traces.uri_mask', [], ['/P2P[0-9A-Za-z]+/' => '{identifier}']],
77+
['open-telemetry.metrics.uri_mask', [], ['/P2P[0-9A-Za-z]+/' => '{identifier}']],
7678
]);
7779
}
7880

@@ -178,6 +180,69 @@ public function testProcessWithSuccessfulRequest(): void
178180
$this->assertSame($this->response, $result);
179181
}
180182

183+
public function testProcessWithUriMaskAndSuccessRequest(): void
184+
{
185+
$this->configureRequestMock('GET', 'https://api.example.com:443/users/P2P123?limit=10');
186+
$this->configureResponseMock(200);
187+
188+
$spanScope = $this->createMock(SpanScope::class);
189+
190+
$propagator = $this->createMock(TextMapPropagatorInterface::class);
191+
$propagator->expects($this->once())
192+
->method('extract')
193+
->with([
194+
'User-Agent' => ['TestAgent/1.0'],
195+
'x-forwarded-for' => ['192.168.1.100'],
196+
'remote-host' => [''],
197+
'x-real-ip' => [''],
198+
])
199+
->willReturn($context = $this->createMock(ContextInterface::class));
200+
201+
$this->instrumentation->expects($this->once())->method('propagator')->willReturn($propagator);
202+
203+
$this->instrumentation->expects($this->once())
204+
->method('startSpan')
205+
->with(
206+
'/users/{identifier}',
207+
SpanKind::KIND_SERVER,
208+
[
209+
HttpAttributes::HTTP_REQUEST_METHOD => 'GET',
210+
UrlAttributes::URL_FULL => 'https://api.example.com:443/users/P2P123?limit=10',
211+
UrlAttributes::URL_PATH => '/users/P2P123',
212+
UrlAttributes::URL_SCHEME => 'https',
213+
UrlAttributes::URL_QUERY => 'limit=10',
214+
ServerAttributes::SERVER_ADDRESS => 'api.example.com',
215+
ServerAttributes::SERVER_PORT => 443,
216+
UserAgentAttributes::USER_AGENT_ORIGINAL => 'TestAgent/1.0',
217+
ClientAttributes::CLIENT_ADDRESS => '192.168.1.100',
218+
],
219+
$this->isType('int'),
220+
$context
221+
)
222+
->willReturn($spanScope);
223+
224+
$spanScope->expects($this->once())
225+
->method('setAttributes')
226+
->with([
227+
HttpAttributes::HTTP_RESPONSE_STATUS_CODE => 200,
228+
HttpIncubatingAttributes::HTTP_RESPONSE_BODY_SIZE => '1024',
229+
'http.response.header.content-type' => 'application/json',
230+
'http.response.header.content-length' => '1024',
231+
]);
232+
233+
$spanScope->expects($this->once())->method('end');
234+
235+
$middleware = new TraceMiddleware(
236+
$this->config,
237+
$this->instrumentation,
238+
$this->switcher
239+
);
240+
241+
$result = $middleware->process($this->request, $this->handler);
242+
243+
$this->assertSame($this->response, $result);
244+
}
245+
181246
public function testProcessWithException(): void
182247
{
183248
$this->configureRequestMock('POST', 'https://api.example.com:443/api/error');

0 commit comments

Comments
 (0)