Skip to content

Commit 93f265f

Browse files
author
Liudmila Molkova
authored
Clientcore - implement metrics support and report http request duration in instrumentation policy (Azure#43957)
* Add metrics
1 parent 08d3dbf commit 93f265f

File tree

43 files changed

+2619
-381
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2619
-381
lines changed

sdk/clientcore/core/spotbugs-exclude.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,11 @@
226226
</Match>
227227
<Match>
228228
<Bug pattern="NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS" />
229-
<Class name="io.clientcore.core.serialization.json.contract.JsonWriterContractTests" />
229+
<Or>
230+
<Class name="io.clientcore.core.serialization.json.contract.JsonWriterContractTests" />
231+
<Class name="io.clientcore.core.implementation.instrumentation.fallback.FallbackInstrumentationTests" />
232+
</Or>
233+
230234
</Match>
231235
<Match>
232236
<Bug pattern="NP_NULL_PARAM_DEREF_NONVIRTUAL" />

sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpInstrumentationOptions.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ public HttpInstrumentationOptions setTracingEnabled(boolean isTracingEnabled) {
250250
return this;
251251
}
252252

253+
@Override
254+
public HttpInstrumentationOptions setMetricsEnabled(boolean isMetricsEnabled) {
255+
super.setMetricsEnabled(isMetricsEnabled);
256+
return this;
257+
}
258+
253259
@Override
254260
public HttpInstrumentationOptions setTelemetryProvider(Object telemetryProvider) {
255261
super.setTelemetryProvider(telemetryProvider);

sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java

Lines changed: 122 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import io.clientcore.core.instrumentation.Instrumentation;
1717
import io.clientcore.core.instrumentation.InstrumentationContext;
1818
import io.clientcore.core.instrumentation.LibraryInstrumentationOptions;
19+
import io.clientcore.core.instrumentation.metrics.DoubleHistogram;
20+
import io.clientcore.core.instrumentation.metrics.Meter;
1921
import io.clientcore.core.instrumentation.tracing.SpanBuilder;
2022
import io.clientcore.core.instrumentation.tracing.TracingScope;
2123
import io.clientcore.core.instrumentation.tracing.Span;
@@ -28,6 +30,7 @@
2830
import java.io.IOException;
2931
import java.io.InputStream;
3032
import java.util.Collections;
33+
import java.util.HashMap;
3134
import java.util.Locale;
3235
import java.util.Map;
3336
import java.util.Properties;
@@ -37,6 +40,7 @@
3740
import java.net.URI;
3841

3942
import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri;
43+
import static io.clientcore.core.implementation.instrumentation.AttributeKeys.ERROR_TYPE_KEY;
4044
import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_CONTENT_KEY;
4145
import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_SIZE_KEY;
4246
import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_DURATION_KEY;
@@ -76,7 +80,7 @@
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&#40;&#41;
@@ -86,10 +90,10 @@
8690
* .build&#40;&#41;;
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
* &#47;&#47; You can configure URL sanitization to include additional query parameters to preserve
@@ -104,10 +108,10 @@
104108
* .build&#40;&#41;;
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 = &#40;request, next&#41; -&gt; &#123;
@@ -130,7 +134,7 @@
130134
*
131135
*
132136
* </pre>
133-
* <!-- end io.clientcore.core.telemetry.tracing.enrichhttpspans -->
137+
* <!-- end io.clientcore.core.instrumentation.enrichhttpspans -->
134138
*
135139
*/
136140
public 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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package io.clientcore.core.implementation.instrumentation;
5+
6+
import io.clientcore.core.instrumentation.InstrumentationAttributes;
7+
8+
import java.util.Objects;
9+
10+
/**
11+
* Noop implementation of {@link InstrumentationAttributes}.
12+
*/
13+
public final class NoopAttributes implements InstrumentationAttributes {
14+
public static final NoopAttributes INSTANCE = new NoopAttributes();
15+
16+
/**
17+
* {@inheritDoc}
18+
*/
19+
@Override
20+
public InstrumentationAttributes put(String key, Object value) {
21+
Objects.requireNonNull(key, "'key' cannot be null.");
22+
Objects.requireNonNull(value, "'value' cannot be null.");
23+
24+
return this;
25+
}
26+
27+
private NoopAttributes() {
28+
}
29+
}

0 commit comments

Comments
 (0)