-
Notifications
You must be signed in to change notification settings - Fork 199
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
OpenTelemetry Pre-aggregated metrics (#2439)
* Add placeholder * Rename * Start mapping pre-agg metrics to azure monitor metrics * Add preagg metrics extractor * Fix spotless * Use stdout * Update test and refactor * Fix test * Fix lgtm * Perf buckets * Hack httpservermetric onEnd * Fix spotless * Shade HttpClientMetrics * Fix conversion * Disable performanceBucket in unit test * Validate pre-agg in smoke test * Remove debug logs * Fix typo * Remove commented out code * Fix rpc * Fix ci tests * Refactor * Add a new smoke test * Clean up smoke test * Add perfromanceBucket to http.client.duration and http.server.duration only * Revert httpclient smoke test * Fix preaggregated metrics smoke test * Delete a todo that has been resolved * Delete stdout * Delete unused dependencies and ivar * Verify rpc pre-aggregated metrics * Generate rpcClientMetrics to check its metric content * Fix NPE and fix perfBucket is not part of rpc metrics attributes * Send rpc/http.client as dependencies/duration and rpc/http.server as requests/duration * Stdout http.server.duration metric * Add target to httpClientMetrics' attributes * Add taret to rpcClientMetrics * Add target to rpcclientmetrics * Fix smoke test * Fix rpc smoke test * Remove stdout suppressWarning * Debug 'ai.user.userAgent' * Remove perf bucket and add isSynthetic * Address comments * Fix compilation errors * Remove unused imports * Add MS_ProcessedByMetricExtractors to request/dependency not pre-agg metrics * Remove an assert * Rename a method * Fix tests * Fix test * Fix ci tests * Fix spotless * Reimplement the logic for adding _MS.ProcessedByMetricExtractors * Remove isPreAggregated attribute * Fix spotless * Address comments * Fix smoke tests * Fix smoke tests * Fix tests * Fix tests * Fix * Debug synthetic * Fix tests * Fix userAgent otel key and more tests * Fix more tests * Fix more tests * Fix one more test * Fix pre-agg metrics' custom dimensions * Revert change and add a todo * Fix pre-agg metrics were failing to be ingested into mdm * Fix tests * Remove a todo * Remove debug logs * Address comments * Add appinsights code comment * Exclude return statement * Use containsExactly * Verify _MS.ProcessedByMetricExtractors * Fix wrong import * Fix a compilation error * Turn baseextractor into a util class * Address comments * Fix tests * Address more comments * Fix tests * Use attrbuteKey constants * Address comments * Fix a typo * Fix one more test * Remove MetricView and turn extractors into util class * Apply the same to rpcservermetrics * Rename * Comments * Use 'Http' for dependency type Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
- Loading branch information
Showing
62 changed files
with
2,124 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
...n/java/io/opentelemetry/instrumentation/api/instrumenter/BootstrapSemanticAttributes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* | ||
* ApplicationInsights-Java | ||
* Copyright (c) Microsoft Corporation | ||
* All rights reserved. | ||
* | ||
* MIT License | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this | ||
* software and associated documentation files (the ""Software""), to deal in the Software | ||
* without restriction, including without limitation the rights to use, copy, modify, merge, | ||
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit | ||
* persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* The above copyright notice and this permission notice shall be included in all copies or | ||
* substantial portions of the Software. | ||
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | ||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | ||
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE | ||
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||
* DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.api.instrumenter; | ||
|
||
import static io.opentelemetry.api.common.AttributeKey.booleanKey; | ||
import static io.opentelemetry.api.common.AttributeKey.stringKey; | ||
|
||
import io.opentelemetry.api.common.AttributeKey; | ||
|
||
public final class BootstrapSemanticAttributes { | ||
|
||
public static final AttributeKey<Boolean> IS_SYNTHETIC = | ||
booleanKey("applicationinsights.internal.is_synthetic"); | ||
public static final AttributeKey<String> TARGET = | ||
stringKey("applicationinsights.internal.target"); | ||
public static final AttributeKey<Boolean> IS_PRE_AGGREGATED = | ||
booleanKey("applicationinsights.internal.is_pre_aggregated"); | ||
|
||
private BootstrapSemanticAttributes() {} | ||
} |
38 changes: 38 additions & 0 deletions
38
...bootstrap/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/UserAgents.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/* | ||
* ApplicationInsights-Java | ||
* Copyright (c) Microsoft Corporation | ||
* All rights reserved. | ||
* | ||
* MIT License | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this | ||
* software and associated documentation files (the ""Software""), to deal in the Software | ||
* without restriction, including without limitation the rights to use, copy, modify, merge, | ||
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit | ||
* persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* The above copyright notice and this permission notice shall be included in all copies or | ||
* substantial portions of the Software. | ||
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | ||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | ||
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE | ||
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||
* DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.api.instrumenter; | ||
|
||
import io.opentelemetry.api.common.Attributes; | ||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; | ||
|
||
public final class UserAgents { | ||
|
||
public static boolean isBot(Attributes endAttributes, Attributes startAttributes) { | ||
String userAgent = endAttributes.get(SemanticAttributes.HTTP_USER_AGENT); | ||
if (userAgent == null) { | ||
userAgent = startAttributes.get(SemanticAttributes.HTTP_USER_AGENT); | ||
} | ||
return userAgent != null && userAgent.contains("AlwaysOn"); | ||
} | ||
|
||
private UserAgents() {} | ||
} |
281 changes: 281 additions & 0 deletions
281
...c/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientMetrics.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
/* | ||
* ApplicationInsights-Java | ||
* Copyright (c) Microsoft Corporation | ||
* All rights reserved. | ||
* | ||
* MIT License | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this | ||
* software and associated documentation files (the ""Software""), to deal in the Software | ||
* without restriction, including without limitation the rights to use, copy, modify, merge, | ||
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit | ||
* persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* The above copyright notice and this permission notice shall be included in all copies or | ||
* substantial portions of the Software. | ||
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | ||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | ||
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE | ||
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||
* DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.api.instrumenter.http; | ||
|
||
import static io.opentelemetry.instrumentation.api.instrumenter.BootstrapSemanticAttributes.IS_PRE_AGGREGATED; | ||
import static io.opentelemetry.instrumentation.api.instrumenter.BootstrapSemanticAttributes.IS_SYNTHETIC; | ||
import static io.opentelemetry.instrumentation.api.instrumenter.BootstrapSemanticAttributes.TARGET; | ||
import static java.util.logging.Level.FINE; | ||
|
||
import com.google.auto.value.AutoValue; | ||
import io.opentelemetry.api.common.AttributeKey; | ||
import io.opentelemetry.api.common.Attributes; | ||
import io.opentelemetry.api.metrics.DoubleHistogram; | ||
import io.opentelemetry.api.metrics.LongHistogram; | ||
import io.opentelemetry.api.metrics.Meter; | ||
import io.opentelemetry.api.trace.Span; | ||
import io.opentelemetry.context.Context; | ||
import io.opentelemetry.context.ContextKey; | ||
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; | ||
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; | ||
import io.opentelemetry.instrumentation.api.instrumenter.UserAgents; | ||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.logging.Logger; | ||
import javax.annotation.Nullable; | ||
|
||
/** | ||
* {@link OperationListener} which keeps track of <a | ||
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-client">HTTP | ||
* client metrics</a>. | ||
*/ | ||
public final class HttpClientMetrics implements OperationListener { | ||
|
||
private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1); | ||
|
||
private static final ContextKey<State> HTTP_CLIENT_REQUEST_METRICS_STATE = | ||
ContextKey.named("http-client-request-metrics-state"); | ||
|
||
private static final Logger logger = Logger.getLogger(HttpClientMetrics.class.getName()); | ||
|
||
/** | ||
* Returns a {@link OperationMetrics} which can be used to enable recording of {@link | ||
* HttpClientMetrics} on an {@link | ||
* io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}. | ||
*/ | ||
public static OperationMetrics get() { | ||
return HttpClientMetrics::new; | ||
} | ||
|
||
private final DoubleHistogram duration; | ||
private final LongHistogram requestSize; | ||
private final LongHistogram responseSize; | ||
|
||
private HttpClientMetrics(Meter meter) { | ||
duration = | ||
meter | ||
.histogramBuilder("http.client.duration") | ||
.setUnit("ms") | ||
.setDescription("The duration of the outbound HTTP request") | ||
.build(); | ||
requestSize = | ||
meter | ||
.histogramBuilder("http.client.request.size") | ||
.setUnit("By") | ||
.setDescription("The size of HTTP request messages") | ||
.ofLongs() | ||
.build(); | ||
responseSize = | ||
meter | ||
.histogramBuilder("http.client.response.size") | ||
.setUnit("By") | ||
.setDescription("The size of HTTP response messages") | ||
.ofLongs() | ||
.build(); | ||
} | ||
|
||
@Override | ||
public Context onStart(Context context, Attributes startAttributes, long startNanos) { | ||
return context.with( | ||
HTTP_CLIENT_REQUEST_METRICS_STATE, | ||
new AutoValue_HttpClientMetrics_State(startAttributes, startNanos)); | ||
} | ||
|
||
@Override | ||
public void onEnd(Context context, Attributes endAttributes, long endNanos) { | ||
State state = context.get(HTTP_CLIENT_REQUEST_METRICS_STATE); | ||
if (state == null) { | ||
logger.log( | ||
FINE, | ||
"No state present when ending context {0}. Cannot record HTTP request metrics.", | ||
context); | ||
return; | ||
} | ||
Attributes durationAndSizeAttributes = | ||
TemporaryMetricsView.applyClientDurationAndSizeView(state.startAttributes(), endAttributes); | ||
|
||
// START APPLICATION INSIGHTS CODE | ||
|
||
// this is needed for detecting telemetry signals that will trigger pre-aggregated metrics via | ||
// auto instrumentations | ||
Span.fromContext(context).setAttribute(IS_PRE_AGGREGATED, true); | ||
|
||
Attributes durationAttributes = | ||
durationAndSizeAttributes.toBuilder() | ||
.put(IS_SYNTHETIC, UserAgents.isBot(endAttributes, state.startAttributes())) | ||
.put(TARGET, getTargetForHttpClientSpan(durationAndSizeAttributes)) | ||
.build(); | ||
|
||
// END APPLICATION INSIGHTS CODE | ||
|
||
this.duration.record( | ||
(endNanos - state.startTimeNanos()) / NANOS_PER_MS, durationAttributes, context); | ||
|
||
Long requestLength = | ||
getAttribute( | ||
SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, endAttributes, state.startAttributes()); | ||
if (requestLength != null) { | ||
requestSize.record(requestLength, durationAndSizeAttributes); | ||
} | ||
Long responseLength = | ||
getAttribute( | ||
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, | ||
endAttributes, | ||
state.startAttributes()); | ||
if (responseLength != null) { | ||
responseSize.record(responseLength, durationAndSizeAttributes); | ||
} | ||
} | ||
|
||
// this is copied from SpanDataMapper | ||
private static String getTargetForHttpClientSpan(Attributes attributes) { | ||
// from the spec, at least one of the following sets of attributes is required: | ||
// * http.url | ||
// * http.scheme, http.host, http.target | ||
// * http.scheme, net.peer.name, net.peer.port, http.target | ||
// * http.scheme, net.peer.ip, net.peer.port, http.target | ||
String target = getTargetFromPeerService(attributes); | ||
if (target != null) { | ||
return target; | ||
} | ||
// note http.host includes the port (at least when non-default) | ||
target = attributes.get(SemanticAttributes.HTTP_HOST); | ||
if (target != null) { | ||
String scheme = attributes.get(SemanticAttributes.HTTP_SCHEME); | ||
if ("http".equals(scheme)) { | ||
if (target.endsWith(":80")) { | ||
target = target.substring(0, target.length() - 3); | ||
} | ||
} else if ("https".equals(scheme)) { | ||
if (target.endsWith(":443")) { | ||
target = target.substring(0, target.length() - 4); | ||
} | ||
} | ||
return target; | ||
} | ||
String url = attributes.get(SemanticAttributes.HTTP_URL); | ||
if (url != null) { | ||
target = getTargetFromUrl(url); | ||
if (target != null) { | ||
return target; | ||
} | ||
} | ||
String scheme = attributes.get(SemanticAttributes.HTTP_SCHEME); | ||
int defaultPort; | ||
if ("http".equals(scheme)) { | ||
defaultPort = 80; | ||
} else if ("https".equals(scheme)) { | ||
defaultPort = 443; | ||
} else { | ||
defaultPort = 0; | ||
} | ||
target = getTargetFromNetAttributes(attributes, defaultPort); | ||
if (target != null) { | ||
return target; | ||
} | ||
// this should not happen, just a failsafe | ||
return "Http"; | ||
} | ||
|
||
// this is copied from SpanDataMapper | ||
@Nullable | ||
private static String getTargetFromPeerService(Attributes attributes) { | ||
// do not append port to peer.service | ||
return attributes.get(SemanticAttributes.PEER_SERVICE); | ||
} | ||
|
||
// this is copied from SpanDataMapper | ||
@Nullable | ||
private static String getTargetFromNetAttributes(Attributes attributes, int defaultPort) { | ||
String target = getHostFromNetAttributes(attributes); | ||
if (target == null) { | ||
return null; | ||
} | ||
// append net.peer.port to target | ||
Long port = attributes.get(SemanticAttributes.NET_PEER_PORT); | ||
if (port != null && port != defaultPort) { | ||
return target + ":" + port; | ||
} | ||
return target; | ||
} | ||
|
||
// this is copied from SpanDataMapper | ||
@Nullable | ||
private static String getHostFromNetAttributes(Attributes attributes) { | ||
String host = attributes.get(SemanticAttributes.NET_PEER_NAME); | ||
if (host != null) { | ||
return host; | ||
} | ||
return attributes.get(SemanticAttributes.NET_PEER_IP); | ||
} | ||
|
||
// this is copied from SpanDataMapper | ||
@Nullable | ||
private static String getTargetFromUrl(String url) { | ||
int schemeEndIndex = url.indexOf(':'); | ||
if (schemeEndIndex == -1) { | ||
// not a valid url | ||
return null; | ||
} | ||
|
||
int len = url.length(); | ||
if (schemeEndIndex + 2 < len | ||
&& url.charAt(schemeEndIndex + 1) == '/' | ||
&& url.charAt(schemeEndIndex + 2) == '/') { | ||
// has authority component | ||
// look for | ||
// '/' - start of path | ||
// '?' or end of string - empty path | ||
int index; | ||
for (index = schemeEndIndex + 3; index < len; index++) { | ||
char c = url.charAt(index); | ||
if (c == '/' || c == '?' || c == '#') { | ||
break; | ||
} | ||
} | ||
String target = url.substring(schemeEndIndex + 3, index); | ||
return target.isEmpty() ? null : target; | ||
} else { | ||
// has no authority | ||
return null; | ||
} | ||
} | ||
|
||
@Nullable | ||
private static <T> T getAttribute(AttributeKey<T> key, Attributes... attributesList) { | ||
for (Attributes attributes : attributesList) { | ||
T value = attributes.get(key); | ||
if (value != null) { | ||
return value; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@AutoValue | ||
abstract static class State { | ||
|
||
abstract Attributes startAttributes(); | ||
|
||
abstract long startTimeNanos(); | ||
} | ||
} |
Oops, something went wrong.