66
77use Composer \InstalledVersions ;
88use GuzzleHttp \Psr7 \Query ;
9+ use OpenTelemetry \API \Common \Time \Clock ;
10+ use OpenTelemetry \API \Common \Time \ClockInterface ;
911use OpenTelemetry \API \Globals ;
1012use OpenTelemetry \API \Instrumentation \CachedInstrumentation ;
1113use OpenTelemetry \API \Trace \Span ;
@@ -81,6 +83,9 @@ class ReactPHPInstrumentation
8183
8284 public static function register (): void
8385 {
86+ /** @var \OpenTelemetry\API\Metrics\HistogramInterface|null */
87+ static $ histogram ;
88+
8489 $ instrumentation = new CachedInstrumentation (
8590 self ::INSTRUMENTATION_LIBRARY_NAME ,
8691 InstalledVersions::getPrettyVersion (self ::COMPOSER_NAME ),
@@ -102,24 +107,28 @@ public static function register(): void
102107 $ request = $ request ->withoutHeader ($ field );
103108 }
104109
105- /** @var non-empty-string|null */
106- $ method = self ::canonizeMethod ($ request ->getMethod ());
110+ /** @var array{'http.request.method':non-empty-string|null,'server.address':non-empty-string,'server.port':int} $requestMeta */
111+ $ requestMeta = [
112+ 'http.request.method ' => self ::canonizeMethod ($ request ->getMethod ()),
113+ 'server.address ' => $ request ->getUri ()->getHost (),
114+ 'server.port ' => $ request ->getUri ()->getPort () ?? ($ request ->getUri ()->getScheme () === 'https ' ? 443 : 80 ),
115+ ];
107116
108117 $ spanBuilder = $ instrumentation
109118 ->tracer ()
110119 // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
111- ->spanBuilder ($ method ?? self ::HTTP_REQUEST_METHOD_HTTP )
120+ ->spanBuilder ($ requestMeta [ ' http.request. method' ] ?? self ::HTTP_REQUEST_METHOD_HTTP )
112121 ->setParent ($ parentContext )
113122 ->setSpanKind (SpanKind::KIND_CLIENT )
114- ->setAttribute (TraceAttributes::HTTP_REQUEST_METHOD , $ method ?? TraceAttributeValues::HTTP_REQUEST_METHOD_OTHER )
115- ->setAttribute (TraceAttributes::SERVER_ADDRESS , $ request -> getUri ()-> getHost () )
116- ->setAttribute (TraceAttributes::SERVER_PORT , $ request -> getUri ()-> getPort () ?? ( $ request -> getUri ()-> getScheme () === ' https ' ? 443 : 80 ) )
123+ ->setAttribute (TraceAttributes::HTTP_REQUEST_METHOD , $ requestMeta [ ' http.request. method' ] ?? TraceAttributeValues::HTTP_REQUEST_METHOD_OTHER )
124+ ->setAttribute (TraceAttributes::SERVER_ADDRESS , $ requestMeta [ ' server.address ' ] )
125+ ->setAttribute (TraceAttributes::SERVER_PORT , $ requestMeta [ ' server.port ' ] )
117126 ->setAttribute (TraceAttributes::URL_FULL , self ::sanitizeUrl ($ request ->getUri ()))
118127 // https://opentelemetry.io/docs/specs/semconv/code/
119128 ->setAttribute (TraceAttributes::CODE_FUNCTION_NAME , sprintf ('%s::%s ' , $ class , $ function ));
120129
121130 // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
122- if ($ method === null ) {
131+ if ($ requestMeta [ ' http.request. method' ] === null ) {
123132 $ spanBuilder ->setAttribute (TraceAttributes::HTTP_REQUEST_METHOD_ORIGINAL , $ request ->getMethod ());
124133 }
125134
@@ -133,7 +142,8 @@ public static function register(): void
133142 $ spanBuilder ->setAttribute (TraceAttributes::CODE_LINE_NUMBER , $ lineno );
134143 }
135144
136- $ span = $ spanBuilder ->startSpan ();
145+ $ requestStart = Clock::getDefault ()->now ();
146+ $ span = $ spanBuilder ->setStartTimestamp ($ requestStart )->startSpan ();
137147 $ context = $ span ->storeInContext ($ parentContext );
138148 $ propagator ->inject ($ request , HeadersPropagator::instance (), $ context );
139149
@@ -146,11 +156,13 @@ public static function register(): void
146156 }
147157 }
148158
149- Context::storage ()->attach ($ context );
159+ $ scope = Context::storage ()->attach ($ context );
160+ $ scope ->offsetSet ('requestMeta ' , $ requestMeta );
161+ $ scope ->offsetSet ('requestStart ' , $ requestStart );
150162
151163 return [$ request ];
152164 },
153- post: static function (Transaction $ transaction , array $ params , PromiseInterface $ promise ): PromiseInterface {
165+ post: static function (Transaction $ transaction , array $ params , PromiseInterface $ promise ) use (& $ histogram , $ instrumentation ) : PromiseInterface {
154166 $ scope = Context::storage ()->scope ();
155167 $ scope ?->detach();
156168
@@ -160,21 +172,42 @@ public static function register(): void
160172
161173 $ span = Span::fromContext ($ scope ->context ());
162174
163- if (!$ span ->isRecording ()) {
175+ //https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client
176+ $ histogram ??= $ instrumentation ->meter ()->createHistogram (
177+ 'http.client.request.duration ' ,
178+ 's ' ,
179+ 'Duration of HTTP client requests. ' ,
180+ ['ExplicitBucketBoundaries ' => [0.005 , 0.01 , 0.025 , 0.05 , 0.075 , 0.1 , 0.25 , 0.5 , 0.75 , 1 , 2.5 , 5 , 7.5 , 10 ]]
181+ );
182+
183+ if (!$ span ->isRecording () && !$ histogram ->isEnabled ()) {
164184 return $ promise ;
165185 }
166186
187+ /** @var array{'http.request.method':non-empty-string|null,'server.address':non-empty-string,'server.port':int} $requestMeta */
188+ $ requestMeta = $ scope ->offsetGet ('requestMeta ' );
189+ $ requestMeta ['http.request.method ' ] ??= '_OTHER ' ;
190+ /** @var int $requestStart */
191+ $ requestStart = $ scope ->offsetGet ('requestStart ' );
192+
167193 return $ promise ->then (
168- onFulfilled: function (ResponseInterface $ response ) use ($ span ) {
194+ onFulfilled: function (ResponseInterface $ response ) use ($ histogram , $ requestMeta , $ requestStart , $ span ) {
195+ $ requestEnd = Clock::getDefault ()->now ();
196+ /** @var array{'http.response.status_code':int,'network.protocol.version':non-empty-string,'error.type'?:non-empty-string} $responseMeta */
197+ $ responseMeta = [
198+ 'http.response.status_code ' => $ response ->getStatusCode (),
199+ 'network.protocol.version ' => $ response ->getProtocolVersion (),
200+ ];
169201 // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
170202 $ span
171- ->setAttribute (TraceAttributes::HTTP_RESPONSE_STATUS_CODE , $ response-> getStatusCode () )
172- ->setAttribute (TraceAttributes::NETWORK_PROTOCOL_VERSION , $ response -> getProtocolVersion () );
203+ ->setAttribute (TraceAttributes::HTTP_RESPONSE_STATUS_CODE , $ responseMeta [ ' http. response.status_code ' ] )
204+ ->setAttribute (TraceAttributes::NETWORK_PROTOCOL_VERSION , $ responseMeta [ ' network.protocol.version ' ] );
173205
174- if ($ response-> getStatusCode () >= 400 && $ response-> getStatusCode () < 600 ) {
206+ if ($ responseMeta [ ' http. response.status_code ' ] >= 400 && $ responseMeta [ ' http. response.status_code ' ] < 600 ) {
175207 $ span
176208 ->setStatus (StatusCode::STATUS_ERROR )
177- ->setAttribute (TraceAttributes::ERROR_TYPE , (string ) $ response ->getStatusCode ());
209+ ->setAttribute (TraceAttributes::ERROR_TYPE , (string ) $ responseMeta ['http.response.status_code ' ]);
210+ $ responseMeta ['error.type ' ] = (string ) $ responseMeta ['http.response.status_code ' ];
178211 }
179212
180213 foreach (explode (', ' , $ _ENV [self ::ENV_HTTP_RESPONSE_HEADERS ] ?? '' ) as $ header ) {
@@ -186,19 +219,31 @@ public static function register(): void
186219 }
187220 }
188221
189- $ span ->end ();
222+ $ span ->end ($ requestEnd );
223+
224+ $ histogram ->record (
225+ (float ) (($ requestEnd - $ requestStart ) / ClockInterface::NANOS_PER_SECOND ),
226+ array_merge ($ requestMeta , $ responseMeta )
227+ );
190228
191229 return $ response ;
192230 },
193- onRejected: function (Throwable $ t ) use ($ span ) {
231+ onRejected: function (Throwable $ t ) use ($ histogram , $ requestMeta , $ requestStart , $ span ) {
232+ $ requestEnd = Clock::getDefault ()->now ();
194233 $ span ->recordException ($ t );
195234 if (is_a ($ t , ResponseException::class)) {
235+ /** @var array{'http.response.status_code':int,'network.protocol.version':non-empty-string,'error.type':non-empty-string} $responseMeta */
236+ $ responseMeta = [
237+ 'error.type ' => (string ) $ t ->getCode (),
238+ 'http.response.status_code ' => $ t ->getCode (),
239+ 'network.protocol.version ' => $ t ->getResponse ()->getProtocolVersion (),
240+ ];
196241 // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
197242 $ span
198243 ->setStatus (StatusCode::STATUS_ERROR )
199- ->setAttribute (TraceAttributes::ERROR_TYPE , ( string ) $ t -> getCode () )
200- ->setAttribute (TraceAttributes::HTTP_RESPONSE_STATUS_CODE , $ t -> getCode () )
201- ->setAttribute (TraceAttributes::NETWORK_PROTOCOL_VERSION , $ t -> getResponse ()-> getProtocolVersion () );
244+ ->setAttribute (TraceAttributes::ERROR_TYPE , $ responseMeta [ ' error.type ' ] )
245+ ->setAttribute (TraceAttributes::HTTP_RESPONSE_STATUS_CODE , $ responseMeta [ ' http.response.status_code ' ] )
246+ ->setAttribute (TraceAttributes::NETWORK_PROTOCOL_VERSION , $ responseMeta [ ' network.protocol.version ' ] );
202247
203248 foreach (explode (', ' , $ _ENV [self ::ENV_HTTP_RESPONSE_HEADERS ] ?? '' ) as $ header ) {
204249 if ($ t ->getResponse ()->hasHeader ($ header )) {
@@ -209,12 +254,21 @@ public static function register(): void
209254 }
210255 }
211256 } else {
257+ /** @var array{'error.type':non-empty-string} $responseMeta */
258+ $ responseMeta = [
259+ 'error.type ' => $ t ::class,
260+ ];
212261 $ span
213262 ->setStatus (StatusCode::STATUS_ERROR , $ t ->getMessage ())
214- ->setAttribute (TraceAttributes::ERROR_TYPE , $ t ::class );
263+ ->setAttribute (TraceAttributes::ERROR_TYPE , $ responseMeta [ ' error.type ' ] );
215264 }
216265
217- $ span ->end ();
266+ $ span ->end ($ requestEnd );
267+
268+ $ histogram ->record (
269+ (float ) (($ requestEnd - $ requestStart ) / ClockInterface::NANOS_PER_SECOND ),
270+ array_merge ($ requestMeta , $ responseMeta )
271+ );
218272
219273 throw $ t ;
220274 }
0 commit comments