66
77use Composer \InstalledVersions ;
88use GuzzleHttp \Psr7 \Query ;
9+ use OpenTelemetry \API \Common \Time \Clock ;
910use OpenTelemetry \API \Globals ;
1011use OpenTelemetry \API \Instrumentation \CachedInstrumentation ;
1112use OpenTelemetry \API \Trace \Span ;
@@ -102,24 +103,28 @@ public static function register(): void
102103 $ request = $ request ->withoutHeader ($ field );
103104 }
104105
105- /** @var non-empty-string|null */
106- $ method = self ::canonizeMethod ($ request ->getMethod ());
106+ /** @psalm-var array{'http.request.method':non-empty-string|null,'server.address':non-empty-string,'server.port':int} */
107+ $ requestMeta = [
108+ 'http.request.method ' => self ::canonizeMethod ($ request ->getMethod ()),
109+ 'server.address ' => $ request ->getUri ()->getHost (),
110+ 'server.port ' => $ request ->getUri ()->getPort () ?? ($ request ->getUri ()->getScheme () === 'https ' ? 443 : 80 ),
111+ ];
107112
108113 $ spanBuilder = $ instrumentation
109114 ->tracer ()
110115 // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
111- ->spanBuilder ($ method ?? self ::HTTP_REQUEST_METHOD_HTTP )
116+ ->spanBuilder ($ requestMeta [ ' http.request. method' ] ?? self ::HTTP_REQUEST_METHOD_HTTP )
112117 ->setParent ($ parentContext )
113118 ->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 ) )
119+ ->setAttribute (TraceAttributes::HTTP_REQUEST_METHOD , $ requestMeta [ ' http.request. method' ] ?? TraceAttributeValues::HTTP_REQUEST_METHOD_OTHER )
120+ ->setAttribute (TraceAttributes::SERVER_ADDRESS , $ requestMeta [ ' server.address ' ] )
121+ ->setAttribute (TraceAttributes::SERVER_PORT , $ requestMeta [ ' server.port ' ] )
117122 ->setAttribute (TraceAttributes::URL_FULL , self ::sanitizeUrl ($ request ->getUri ()))
118123 // https://opentelemetry.io/docs/specs/semconv/code/
119124 ->setAttribute (TraceAttributes::CODE_FUNCTION_NAME , sprintf ('%s::%s ' , $ class , $ function ));
120125
121126 // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
122- if ($ method === null ) {
127+ if ($ requestMeta [ ' http.request. method' ] === null ) {
123128 $ spanBuilder ->setAttribute (TraceAttributes::HTTP_REQUEST_METHOD_ORIGINAL , $ request ->getMethod ());
124129 }
125130
@@ -133,7 +138,8 @@ public static function register(): void
133138 $ spanBuilder ->setAttribute (TraceAttributes::CODE_LINE_NUMBER , $ lineno );
134139 }
135140
136- $ span = $ spanBuilder ->startSpan ();
141+ $ requestStart = Clock::getDefault ()->now ();
142+ $ span = $ spanBuilder ->setStartTimestamp ($ requestStart )->startSpan ();
137143 $ context = $ span ->storeInContext ($ parentContext );
138144 $ propagator ->inject ($ request , HeadersPropagator::instance (), $ context );
139145
@@ -146,11 +152,13 @@ public static function register(): void
146152 }
147153 }
148154
149- Context::storage ()->attach ($ context );
155+ $ scope = Context::storage ()->attach ($ context );
156+ $ scope ->offsetSet ('requestMeta ' , $ requestMeta );
157+ $ scope ->offsetSet ('requestStart ' , $ requestStart );
150158
151159 return [$ request ];
152160 },
153- post: static function (Transaction $ transaction , array $ params , PromiseInterface $ promise ): PromiseInterface {
161+ post: static function (Transaction $ transaction , array $ params , PromiseInterface $ promise ) use ( $ instrumentation ) : PromiseInterface {
154162 $ scope = Context::storage ()->scope ();
155163 $ scope ?->detach();
156164
@@ -164,17 +172,29 @@ public static function register(): void
164172 return $ promise ;
165173 }
166174
175+ /** @psalm-var array{'http.request.method':non-empty-string|null,'server.address':non-empty-string,'server.port':int} */
176+ $ requestMeta = $ scope ->offsetGet ('requestMeta ' );
177+ $ requestMeta ['http.request.method ' ] ??= '_OTHER ' ;
178+ $ requestStart = $ scope ->offsetGet ('requestStart ' );
179+
167180 return $ promise ->then (
168- onFulfilled: function (ResponseInterface $ response ) use ($ span ) {
181+ onFulfilled: function (ResponseInterface $ response ) use ($ instrumentation , $ requestMeta , $ requestStart , $ span ) {
182+ $ requestEnd = Clock::getDefault ()->now ();
183+ /** @psalm-var array{'http.response.status_code':int,'network.protocol.version':non-empty-string,'error.type'?:non-empty-string} */
184+ $ responseMeta = [
185+ 'http.response.status_code ' => $ response ->getStatusCode (),
186+ 'network.protocol.version ' => $ response ->getProtocolVersion (),
187+ ];
169188 // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
170189 $ span
171- ->setAttribute (TraceAttributes::HTTP_RESPONSE_STATUS_CODE , $ response-> getStatusCode () )
172- ->setAttribute (TraceAttributes::NETWORK_PROTOCOL_VERSION , $ response -> getProtocolVersion () );
190+ ->setAttribute (TraceAttributes::HTTP_RESPONSE_STATUS_CODE , $ responseMeta [ ' http. response.status_code ' ] )
191+ ->setAttribute (TraceAttributes::NETWORK_PROTOCOL_VERSION , $ responseMeta [ ' network.protocol.version ' ] );
173192
174- if ($ response-> getStatusCode () >= 400 && $ response-> getStatusCode () < 600 ) {
193+ if ($ responseMeta [ ' http. response.status_code ' ] >= 400 && $ responseMeta [ ' http. response.status_code ' ] < 600 ) {
175194 $ span
176195 ->setStatus (StatusCode::STATUS_ERROR )
177- ->setAttribute (TraceAttributes::ERROR_TYPE , (string ) $ response ->getStatusCode ());
196+ ->setAttribute (TraceAttributes::ERROR_TYPE , (string ) $ responseMeta ['http.response.status_code ' ]);
197+ $ responseMeta ['error.type ' ] = (string ) $ responseMeta ['http.response.status_code ' ];
178198 }
179199
180200 foreach (explode (', ' , $ _ENV [self ::ENV_HTTP_RESPONSE_HEADERS ] ?? '' ) as $ header ) {
@@ -186,19 +206,34 @@ public static function register(): void
186206 }
187207 }
188208
189- $ span ->end ();
209+ $ span ->end ($ requestEnd );
210+
211+ //https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client
212+ $ instrumentation ->meter ()->createHistogram (
213+ 'http.client.request.duration ' ,
214+ 's ' ,
215+ 'Duration of HTTP client requests. ' ,
216+ ['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 ]]
217+ )->record ($ requestEnd - $ requestStart , array_merge ($ requestMeta , $ responseMeta ));
190218
191219 return $ response ;
192220 },
193- onRejected: function (Throwable $ t ) use ($ span ) {
221+ onRejected: function (Throwable $ t ) use ($ instrumentation , $ requestMeta , $ requestStart , $ span ) {
222+ $ requestEnd = Clock::getDefault ()->now ();
194223 $ span ->recordException ($ t );
195224 if (is_a ($ t , ResponseException::class)) {
225+ /** @psalm-var array{'http.response.status_code':int,'network.protocol.version':non-empty-string,'error.type':non-empty-string} */
226+ $ responseMeta = [
227+ 'error.type ' => (string ) $ t ->getCode (),
228+ 'http.response.status_code ' => $ t ->getCode (),
229+ 'network.protocol.version ' => $ t ->getResponse ()->getProtocolVersion (),
230+ ];
196231 // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
197232 $ span
198233 ->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 () );
234+ ->setAttribute (TraceAttributes::ERROR_TYPE , $ responseMeta [ ' error.type ' ] )
235+ ->setAttribute (TraceAttributes::HTTP_RESPONSE_STATUS_CODE , $ responseMeta [ ' http.response.status_code ' ] )
236+ ->setAttribute (TraceAttributes::NETWORK_PROTOCOL_VERSION , $ responseMeta [ ' network.protocol.version ' ] );
202237
203238 foreach (explode (', ' , $ _ENV [self ::ENV_HTTP_RESPONSE_HEADERS ] ?? '' ) as $ header ) {
204239 if ($ t ->getResponse ()->hasHeader ($ header )) {
@@ -209,12 +244,24 @@ public static function register(): void
209244 }
210245 }
211246 } else {
247+ /** @psalm-var array{'error.type':non-empty-string} */
248+ $ responseMeta = [
249+ 'error.type ' => $ t ::class,
250+ ];
212251 $ span
213252 ->setStatus (StatusCode::STATUS_ERROR , $ t ->getMessage ())
214- ->setAttribute (TraceAttributes::ERROR_TYPE , $ t ::class );
253+ ->setAttribute (TraceAttributes::ERROR_TYPE , $ responseMeta [ ' error.type ' ] );
215254 }
216255
217- $ span ->end ();
256+ $ span ->end ($ requestEnd );
257+
258+ //https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client
259+ $ instrumentation ->meter ()->createHistogram (
260+ 'http.client.request.duration ' ,
261+ 's ' ,
262+ 'Duration of HTTP client requests. ' ,
263+ ['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 ]]
264+ )->record ($ requestEnd - $ requestStart , array_merge ($ requestMeta , $ responseMeta ));
218265
219266 throw $ t ;
220267 }
0 commit comments