1616import io .clientcore .core .instrumentation .Instrumentation ;
1717import io .clientcore .core .instrumentation .InstrumentationContext ;
1818import io .clientcore .core .instrumentation .LibraryInstrumentationOptions ;
19+ import io .clientcore .core .instrumentation .metrics .DoubleHistogram ;
20+ import io .clientcore .core .instrumentation .metrics .Meter ;
1921import io .clientcore .core .instrumentation .tracing .SpanBuilder ;
2022import io .clientcore .core .instrumentation .tracing .TracingScope ;
2123import io .clientcore .core .instrumentation .tracing .Span ;
2830import java .io .IOException ;
2931import java .io .InputStream ;
3032import java .util .Collections ;
33+ import java .util .HashMap ;
3134import java .util .Locale ;
3235import java .util .Map ;
3336import java .util .Properties ;
3740import java .net .URI ;
3841
3942import static io .clientcore .core .implementation .UrlRedactionUtil .getRedactedUri ;
43+ import static io .clientcore .core .implementation .instrumentation .AttributeKeys .ERROR_TYPE_KEY ;
4044import static io .clientcore .core .implementation .instrumentation .AttributeKeys .HTTP_REQUEST_BODY_CONTENT_KEY ;
4145import static io .clientcore .core .implementation .instrumentation .AttributeKeys .HTTP_REQUEST_BODY_SIZE_KEY ;
4246import static io .clientcore .core .implementation .instrumentation .AttributeKeys .HTTP_REQUEST_DURATION_KEY ;
7680 * so that it's executed in the scope of the span created by the {@link HttpInstrumentationPolicy}.
7781 *
7882 * <p><strong>Configure instrumentation policy:</strong></p>
79- * <!-- src_embed io.clientcore.core.telemetry.tracing .instrumentationpolicy -->
83+ * <!-- src_embed io.clientcore.core.instrumentation .instrumentationpolicy -->
8084 * <pre>
8185 *
8286 * HttpPipeline pipeline = new HttpPipelineBuilder()
8690 * .build();
8791 *
8892 * </pre>
89- * <!-- end io.clientcore.core.telemetry.tracing .instrumentationpolicy -->
93+ * <!-- end io.clientcore.core.instrumentation .instrumentationpolicy -->
9094 *
9195 * <p><strong>Customize instrumentation policy:</strong></p>
92- * <!-- src_embed io.clientcore.core.telemetry.tracing .customizeinstrumentationpolicy -->
96+ * <!-- src_embed io.clientcore.core.instrumentation .customizeinstrumentationpolicy -->
9397 * <pre>
9498 *
9599 * // You can configure URL sanitization to include additional query parameters to preserve
104108 * .build();
105109 *
106110 * </pre>
107- * <!-- end io.clientcore.core.telemetry.tracing .customizeinstrumentationpolicy -->
111+ * <!-- end io.clientcore.core.instrumentation .customizeinstrumentationpolicy -->
108112 *
109113 * <p><strong>Enrich HTTP spans with additional attributes:</strong></p>
110- * <!-- src_embed io.clientcore.core.telemetry.tracing .enrichhttpspans -->
114+ * <!-- src_embed io.clientcore.core.instrumentation .enrichhttpspans -->
111115 * <pre>
112116 *
113117 * HttpPipelinePolicy enrichingPolicy = (request, next) -> {
130134 *
131135 *
132136 * </pre>
133- * <!-- end io.clientcore.core.telemetry.tracing .enrichhttpspans -->
137+ * <!-- end io.clientcore.core.instrumentation .enrichhttpspans -->
134138 *
135139 */
136140public final class HttpInstrumentationPolicy implements HttpPipelinePolicy {
@@ -160,13 +164,23 @@ public final class HttpInstrumentationPolicy implements HttpPipelinePolicy {
160164
161165 private static final int MAX_BODY_LOG_SIZE = 1024 * 16 ;
162166 private static final String REDACTED_PLACEHOLDER = "REDACTED" ;
167+ // HTTP request duration metric is formally defined in the OpenTelemetry Semantic Conventions:
168+ // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-metrics.md#metric-httpclientrequestduration
169+ private static final String REQUEST_DURATION_METRIC_NAME = "http.client.request.duration" ;
170+ private static final String REQUEST_DURATION_METRIC_DESCRIPTION = "Duration of HTTP client requests" ;
171+ private static final String REQUEST_DURATION_METRIC_UNIT = "s" ;
163172
164173 // request log level is low (verbose) since almost all request details are also
165174 // captured on the response log.
166175 private static final ClientLogger .LogLevel HTTP_REQUEST_LOG_LEVEL = ClientLogger .LogLevel .VERBOSE ;
167176 private static final ClientLogger .LogLevel HTTP_RESPONSE_LOG_LEVEL = ClientLogger .LogLevel .INFORMATIONAL ;
168177
169178 private final Tracer tracer ;
179+ private final Meter meter ;
180+ private final boolean isTracingEnabled ;
181+ private final boolean isMetricsEnabled ;
182+ private final Instrumentation instrumentation ;
183+ private final DoubleHistogram httpRequestDuration ;
170184 private final TraceContextPropagator traceContextPropagator ;
171185 private final Set <String > allowedQueryParameterNames ;
172186 private final Set <HttpHeaderName > allowedHeaderNames ;
@@ -179,8 +193,11 @@ public final class HttpInstrumentationPolicy implements HttpPipelinePolicy {
179193 * @param instrumentationOptions Application telemetry options.
180194 */
181195 public HttpInstrumentationPolicy (HttpInstrumentationOptions instrumentationOptions ) {
182- Instrumentation instrumentation = Instrumentation .create (instrumentationOptions , LIBRARY_OPTIONS );
183- this .tracer = instrumentation .getTracer ();
196+ this .instrumentation = Instrumentation .create (instrumentationOptions , LIBRARY_OPTIONS );
197+ this .tracer = instrumentation .createTracer ();
198+ this .meter = instrumentation .createMeter ();
199+ this .httpRequestDuration = meter .createDoubleHistogram (REQUEST_DURATION_METRIC_NAME ,
200+ REQUEST_DURATION_METRIC_DESCRIPTION , REQUEST_DURATION_METRIC_UNIT );
184201 this .traceContextPropagator = instrumentation .getW3CTraceContextPropagator ();
185202
186203 HttpInstrumentationOptions optionsToUse
@@ -195,6 +212,9 @@ public HttpInstrumentationPolicy(HttpInstrumentationOptions instrumentationOptio
195212 .stream ()
196213 .map (queryParamName -> queryParamName .toLowerCase (Locale .ROOT ))
197214 .collect (Collectors .toSet ());
215+
216+ this .isTracingEnabled = tracer .isEnabled ();
217+ this .isMetricsEnabled = meter .isEnabled ();
198218 }
199219
200220 /**
@@ -203,37 +223,38 @@ public HttpInstrumentationPolicy(HttpInstrumentationOptions instrumentationOptio
203223 @ SuppressWarnings ("try" )
204224 @ Override
205225 public Response <?> process (HttpRequest request , HttpPipelineNextPolicy next ) {
206- boolean isTracingEnabled = tracer .isEnabled ();
207- if (!isTracingEnabled && !isLoggingEnabled ) {
226+ if (!isTracingEnabled && !isLoggingEnabled && !isMetricsEnabled ) {
208227 return next .process ();
209228 }
210229
211230 ClientLogger logger = getLogger (request );
212231 final long startNs = System .nanoTime ();
213- String redactedUrl = getRedactedUri (request .getUri (), allowedQueryParameterNames );
214- int tryCount = HttpRequestAccessHelper .getTryCount (request );
232+ final String redactedUrl = getRedactedUri (request .getUri (), allowedQueryParameterNames );
233+ final int tryCount = HttpRequestAccessHelper .getTryCount (request );
215234 final long requestContentLength = getContentLength (logger , request .getBody (), request .getHeaders (), true );
216235
217- InstrumentationContext instrumentationContext
218- = request .getRequestOptions () == null ? null : request .getRequestOptions ().getInstrumentationContext ();
219- Span span = Span .noop ();
220- if (isTracingEnabled ) {
221- if (request .getRequestOptions () == null || request .getRequestOptions () == RequestOptions .none ()) {
222- request .setRequestOptions (new RequestOptions ());
223- }
224-
225- span = startHttpSpan (request , redactedUrl , instrumentationContext );
226- instrumentationContext = span .getInstrumentationContext ();
227- request .getRequestOptions ().setInstrumentationContext (instrumentationContext );
236+ Map <String , Object > metricAttributes = isMetricsEnabled ? new HashMap <>(8 ) : null ;
237+ if (request .getRequestOptions () == null || request .getRequestOptions () == RequestOptions .none ()) {
238+ request .setRequestOptions (new RequestOptions ());
228239 }
229240
230- // even if tracing is disabled, we could have a valid context to propagate
231- // if it was provided by the application explicitly.
232- if (instrumentationContext != null && instrumentationContext .isValid ()) {
233- traceContextPropagator .inject (instrumentationContext , request .getHeaders (), SETTER );
241+ InstrumentationContext parentContext = request .getRequestOptions ().getInstrumentationContext ();
242+
243+ SpanBuilder spanBuilder = tracer .spanBuilder (request .getHttpMethod ().toString (), CLIENT , parentContext );
244+ setStartAttributes (request , redactedUrl , spanBuilder , metricAttributes );
245+ Span span = spanBuilder .startSpan ();
246+
247+ InstrumentationContext context
248+ = span .getInstrumentationContext ().isValid () ? span .getInstrumentationContext () : parentContext ;
249+
250+ if (context != null && context .isValid ()) {
251+ request .getRequestOptions ().setInstrumentationContext (context );
252+ // even if tracing is disabled, we could have a valid context to propagate
253+ // if it was provided by the application explicitly.
254+ traceContextPropagator .inject (context , request .getHeaders (), SETTER );
234255 }
235256
236- logRequest (logger , request , startNs , requestContentLength , redactedUrl , tryCount , instrumentationContext );
257+ logRequest (logger , request , startNs , requestContentLength , redactedUrl , tryCount , context );
237258
238259 try (TracingScope scope = span .makeCurrent ()) {
239260 Response <?> response = next .process ();
@@ -249,75 +270,114 @@ public Response<?> process(HttpRequest request, HttpPipelineNextPolicy next) {
249270 return null ;
250271 }
251272
252- addDetails (request , response , tryCount , span );
253- response = logResponse (logger , response , startNs , requestContentLength , redactedUrl , tryCount ,
254- instrumentationContext );
273+ addDetails (request , response .getStatusCode (), tryCount , span , metricAttributes );
274+ response = logResponse (logger , response , startNs , requestContentLength , redactedUrl , tryCount , context );
255275 span .end ();
256276 return response ;
257277 } catch (RuntimeException t ) {
258- span .end (unwrap (t ));
259- // TODO (limolkova) test otel scope still covers this
278+ Throwable cause = unwrap (t );
279+ if (metricAttributes != null ) {
280+ metricAttributes .put (ERROR_TYPE_KEY , cause .getClass ().getCanonicalName ());
281+ }
282+ span .end (cause );
260283 throw logException (logger , request , null , t , startNs , null , requestContentLength , redactedUrl , tryCount ,
261- instrumentationContext );
284+ context );
285+ } finally {
286+ if (isMetricsEnabled ) {
287+ httpRequestDuration .record ((System .nanoTime () - startNs ) / 1_000_000_000.0 ,
288+ instrumentation .createAttributes (metricAttributes ), context );
289+ }
262290 }
263291 }
264292
265- private Span startHttpSpan (HttpRequest request , String sanitizedUrl , InstrumentationContext context ) {
266- SpanBuilder spanBuilder = tracer .spanBuilder (request .getHttpMethod ().toString (), CLIENT , context )
267- .setAttribute (HTTP_REQUEST_METHOD_KEY , request .getHttpMethod ().toString ())
268- .setAttribute (URL_FULL_KEY , sanitizedUrl )
269- .setAttribute (SERVER_ADDRESS_KEY , request .getUri ().getHost ());
270- maybeSetServerPort (spanBuilder , request .getUri ());
271- return spanBuilder .startSpan ();
293+ private void setStartAttributes (HttpRequest request , String sanitizedUrl , SpanBuilder spanBuilder ,
294+ Map <String , Object > metricAttributes ) {
295+ if (!isTracingEnabled && !isMetricsEnabled ) {
296+ return ;
297+ }
298+
299+ int port = getServerPort (request .getUri ());
300+ if (isTracingEnabled ) {
301+ spanBuilder .setAttribute (HTTP_REQUEST_METHOD_KEY , request .getHttpMethod ().toString ())
302+ .setAttribute (URL_FULL_KEY , sanitizedUrl )
303+ .setAttribute (SERVER_ADDRESS_KEY , request .getUri ().getHost ());
304+
305+ if (port > 0 ) {
306+ spanBuilder .setAttribute (SERVER_PORT_KEY , port );
307+ }
308+ }
309+
310+ if (isMetricsEnabled ) {
311+ metricAttributes .put (HTTP_REQUEST_METHOD_KEY , request .getHttpMethod ().toString ());
312+ metricAttributes .put (SERVER_ADDRESS_KEY , request .getUri ().getHost ());
313+ if (port > 0 ) {
314+ metricAttributes .put (SERVER_PORT_KEY , port );
315+ }
316+ }
272317 }
273318
274319 /**
275320 * Does the best effort to capture the server port with minimum perf overhead.
276321 * If port is not set, we check scheme for "http" and "https" (case-sensitive).
277- * If scheme is not one of those, we don't set the port .
322+ * If scheme is not one of those, returns -1 .
278323 *
279- * @param spanBuilder span builder
280324 * @param uri request URI
281325 */
282- private static void maybeSetServerPort ( SpanBuilder spanBuilder , URI uri ) {
326+ private static int getServerPort ( URI uri ) {
283327 int port = uri .getPort ();
284- if (port != -1 ) {
285- spanBuilder .setAttribute (SERVER_PORT_KEY , port );
286- } else {
328+ if (port == -1 ) {
287329 switch (uri .getScheme ()) {
288330 case "http" :
289- spanBuilder .setAttribute (SERVER_PORT_KEY , 80 );
290- break ;
331+ return 80 ;
291332
292333 case "https" :
293- spanBuilder .setAttribute (SERVER_PORT_KEY , 443 );
294- break ;
334+ return 443 ;
295335
296336 default :
297337 break ;
298338 }
299339 }
340+ return port ;
300341 }
301342
302- private void addDetails (HttpRequest request , Response <?> response , int tryCount , Span span ) {
303- if (!span .isRecording ()) {
343+ private void addDetails (HttpRequest request , int statusCode , int tryCount , Span span ,
344+ Map <String , Object > metricAttributes ) {
345+ if (!span .isRecording () && !isMetricsEnabled ) {
304346 return ;
305347 }
306348
307- span .setAttribute (HTTP_RESPONSE_STATUS_CODE_KEY , (long ) response .getStatusCode ());
308-
309- if (tryCount > 0 ) {
310- span .setAttribute (HTTP_REQUEST_RESEND_COUNT_KEY , (long ) tryCount );
349+ String error = null ;
350+ if (statusCode >= 400 ) {
351+ error = String .valueOf (statusCode );
311352 }
312353
313- String userAgent = request .getHeaders ().getValue (HttpHeaderName .USER_AGENT );
314- if (userAgent != null ) {
315- span .setAttribute (USER_AGENT_ORIGINAL_KEY , userAgent );
354+ if (span .isRecording ()) {
355+ span .setAttribute (HTTP_RESPONSE_STATUS_CODE_KEY , (long ) statusCode );
356+
357+ if (tryCount > 0 ) {
358+ span .setAttribute (HTTP_REQUEST_RESEND_COUNT_KEY , (long ) tryCount );
359+ }
360+
361+ String userAgent = request .getHeaders ().getValue (HttpHeaderName .USER_AGENT );
362+ if (userAgent != null ) {
363+ span .setAttribute (USER_AGENT_ORIGINAL_KEY , userAgent );
364+ }
365+
366+ if (error != null ) {
367+ span .setError (error );
368+ }
316369 }
317370
318- if (response .getStatusCode () >= 400 ) {
319- span .setError (String .valueOf (response .getStatusCode ()));
371+ if (isMetricsEnabled ) {
372+ if (statusCode > 0 ) {
373+ metricAttributes .put (HTTP_RESPONSE_STATUS_CODE_KEY , statusCode );
374+ }
375+
376+ if (error != null ) {
377+ metricAttributes .put (ERROR_TYPE_KEY , error );
378+ }
320379 }
380+
321381 // TODO (lmolkova) url.template and experimental features
322382 }
323383
0 commit comments