Skip to content

Commit

Permalink
OpenTelemetry Pre-aggregated metrics (#2439)
Browse files Browse the repository at this point in the history
* 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
heyams and trask authored Aug 26, 2022
1 parent fbffa16 commit 2170998
Show file tree
Hide file tree
Showing 62 changed files with 2,124 additions and 109 deletions.
5 changes: 5 additions & 0 deletions agent/agent-bootstrap/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ dependencies {
// needed to access io.opentelemetry.instrumentation.api.aisdk.MicrometerUtil
// TODO (heya) remove this when updating to upstream micrometer instrumentation
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
compileOnly("io.opentelemetry:opentelemetry-semconv")
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv")

compileOnly("com.google.auto.value:auto-value-annotations")
annotationProcessor("com.google.auto.value:auto-value")

implementation("ch.qos.logback:logback-classic")
implementation("ch.qos.logback.contrib:logback-json-classic")
Expand Down
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() {}
}
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() {}
}
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();
}
}
Loading

0 comments on commit 2170998

Please sign in to comment.