Skip to content

Commit 2170998

Browse files
heyamstrask
andauthored
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 <[email protected]>
1 parent fbffa16 commit 2170998

File tree

62 files changed

+2124
-109
lines changed

Some content is hidden

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

62 files changed

+2124
-109
lines changed

agent/agent-bootstrap/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ dependencies {
77
// needed to access io.opentelemetry.instrumentation.api.aisdk.MicrometerUtil
88
// TODO (heya) remove this when updating to upstream micrometer instrumentation
99
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
10+
compileOnly("io.opentelemetry:opentelemetry-semconv")
11+
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv")
12+
13+
compileOnly("com.google.auto.value:auto-value-annotations")
14+
annotationProcessor("com.google.auto.value:auto-value")
1015

1116
implementation("ch.qos.logback:logback-classic")
1217
implementation("ch.qos.logback.contrib:logback-json-classic")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* ApplicationInsights-Java
3+
* Copyright (c) Microsoft Corporation
4+
* All rights reserved.
5+
*
6+
* MIT License
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
8+
* software and associated documentation files (the ""Software""), to deal in the Software
9+
* without restriction, including without limitation the rights to use, copy, modify, merge,
10+
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11+
* persons to whom the Software is furnished to do so, subject to the following conditions:
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16+
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17+
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
* DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package io.opentelemetry.instrumentation.api.instrumenter;
23+
24+
import static io.opentelemetry.api.common.AttributeKey.booleanKey;
25+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
26+
27+
import io.opentelemetry.api.common.AttributeKey;
28+
29+
public final class BootstrapSemanticAttributes {
30+
31+
public static final AttributeKey<Boolean> IS_SYNTHETIC =
32+
booleanKey("applicationinsights.internal.is_synthetic");
33+
public static final AttributeKey<String> TARGET =
34+
stringKey("applicationinsights.internal.target");
35+
public static final AttributeKey<Boolean> IS_PRE_AGGREGATED =
36+
booleanKey("applicationinsights.internal.is_pre_aggregated");
37+
38+
private BootstrapSemanticAttributes() {}
39+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* ApplicationInsights-Java
3+
* Copyright (c) Microsoft Corporation
4+
* All rights reserved.
5+
*
6+
* MIT License
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
8+
* software and associated documentation files (the ""Software""), to deal in the Software
9+
* without restriction, including without limitation the rights to use, copy, modify, merge,
10+
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11+
* persons to whom the Software is furnished to do so, subject to the following conditions:
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16+
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17+
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
* DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package io.opentelemetry.instrumentation.api.instrumenter;
23+
24+
import io.opentelemetry.api.common.Attributes;
25+
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
26+
27+
public final class UserAgents {
28+
29+
public static boolean isBot(Attributes endAttributes, Attributes startAttributes) {
30+
String userAgent = endAttributes.get(SemanticAttributes.HTTP_USER_AGENT);
31+
if (userAgent == null) {
32+
userAgent = startAttributes.get(SemanticAttributes.HTTP_USER_AGENT);
33+
}
34+
return userAgent != null && userAgent.contains("AlwaysOn");
35+
}
36+
37+
private UserAgents() {}
38+
}
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/*
2+
* ApplicationInsights-Java
3+
* Copyright (c) Microsoft Corporation
4+
* All rights reserved.
5+
*
6+
* MIT License
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
8+
* software and associated documentation files (the ""Software""), to deal in the Software
9+
* without restriction, including without limitation the rights to use, copy, modify, merge,
10+
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11+
* persons to whom the Software is furnished to do so, subject to the following conditions:
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16+
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17+
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
* DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package io.opentelemetry.instrumentation.api.instrumenter.http;
23+
24+
import static io.opentelemetry.instrumentation.api.instrumenter.BootstrapSemanticAttributes.IS_PRE_AGGREGATED;
25+
import static io.opentelemetry.instrumentation.api.instrumenter.BootstrapSemanticAttributes.IS_SYNTHETIC;
26+
import static io.opentelemetry.instrumentation.api.instrumenter.BootstrapSemanticAttributes.TARGET;
27+
import static java.util.logging.Level.FINE;
28+
29+
import com.google.auto.value.AutoValue;
30+
import io.opentelemetry.api.common.AttributeKey;
31+
import io.opentelemetry.api.common.Attributes;
32+
import io.opentelemetry.api.metrics.DoubleHistogram;
33+
import io.opentelemetry.api.metrics.LongHistogram;
34+
import io.opentelemetry.api.metrics.Meter;
35+
import io.opentelemetry.api.trace.Span;
36+
import io.opentelemetry.context.Context;
37+
import io.opentelemetry.context.ContextKey;
38+
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener;
39+
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics;
40+
import io.opentelemetry.instrumentation.api.instrumenter.UserAgents;
41+
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
42+
import java.util.concurrent.TimeUnit;
43+
import java.util.logging.Logger;
44+
import javax.annotation.Nullable;
45+
46+
/**
47+
* {@link OperationListener} which keeps track of <a
48+
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-client">HTTP
49+
* client metrics</a>.
50+
*/
51+
public final class HttpClientMetrics implements OperationListener {
52+
53+
private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1);
54+
55+
private static final ContextKey<State> HTTP_CLIENT_REQUEST_METRICS_STATE =
56+
ContextKey.named("http-client-request-metrics-state");
57+
58+
private static final Logger logger = Logger.getLogger(HttpClientMetrics.class.getName());
59+
60+
/**
61+
* Returns a {@link OperationMetrics} which can be used to enable recording of {@link
62+
* HttpClientMetrics} on an {@link
63+
* io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder}.
64+
*/
65+
public static OperationMetrics get() {
66+
return HttpClientMetrics::new;
67+
}
68+
69+
private final DoubleHistogram duration;
70+
private final LongHistogram requestSize;
71+
private final LongHistogram responseSize;
72+
73+
private HttpClientMetrics(Meter meter) {
74+
duration =
75+
meter
76+
.histogramBuilder("http.client.duration")
77+
.setUnit("ms")
78+
.setDescription("The duration of the outbound HTTP request")
79+
.build();
80+
requestSize =
81+
meter
82+
.histogramBuilder("http.client.request.size")
83+
.setUnit("By")
84+
.setDescription("The size of HTTP request messages")
85+
.ofLongs()
86+
.build();
87+
responseSize =
88+
meter
89+
.histogramBuilder("http.client.response.size")
90+
.setUnit("By")
91+
.setDescription("The size of HTTP response messages")
92+
.ofLongs()
93+
.build();
94+
}
95+
96+
@Override
97+
public Context onStart(Context context, Attributes startAttributes, long startNanos) {
98+
return context.with(
99+
HTTP_CLIENT_REQUEST_METRICS_STATE,
100+
new AutoValue_HttpClientMetrics_State(startAttributes, startNanos));
101+
}
102+
103+
@Override
104+
public void onEnd(Context context, Attributes endAttributes, long endNanos) {
105+
State state = context.get(HTTP_CLIENT_REQUEST_METRICS_STATE);
106+
if (state == null) {
107+
logger.log(
108+
FINE,
109+
"No state present when ending context {0}. Cannot record HTTP request metrics.",
110+
context);
111+
return;
112+
}
113+
Attributes durationAndSizeAttributes =
114+
TemporaryMetricsView.applyClientDurationAndSizeView(state.startAttributes(), endAttributes);
115+
116+
// START APPLICATION INSIGHTS CODE
117+
118+
// this is needed for detecting telemetry signals that will trigger pre-aggregated metrics via
119+
// auto instrumentations
120+
Span.fromContext(context).setAttribute(IS_PRE_AGGREGATED, true);
121+
122+
Attributes durationAttributes =
123+
durationAndSizeAttributes.toBuilder()
124+
.put(IS_SYNTHETIC, UserAgents.isBot(endAttributes, state.startAttributes()))
125+
.put(TARGET, getTargetForHttpClientSpan(durationAndSizeAttributes))
126+
.build();
127+
128+
// END APPLICATION INSIGHTS CODE
129+
130+
this.duration.record(
131+
(endNanos - state.startTimeNanos()) / NANOS_PER_MS, durationAttributes, context);
132+
133+
Long requestLength =
134+
getAttribute(
135+
SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, endAttributes, state.startAttributes());
136+
if (requestLength != null) {
137+
requestSize.record(requestLength, durationAndSizeAttributes);
138+
}
139+
Long responseLength =
140+
getAttribute(
141+
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
142+
endAttributes,
143+
state.startAttributes());
144+
if (responseLength != null) {
145+
responseSize.record(responseLength, durationAndSizeAttributes);
146+
}
147+
}
148+
149+
// this is copied from SpanDataMapper
150+
private static String getTargetForHttpClientSpan(Attributes attributes) {
151+
// from the spec, at least one of the following sets of attributes is required:
152+
// * http.url
153+
// * http.scheme, http.host, http.target
154+
// * http.scheme, net.peer.name, net.peer.port, http.target
155+
// * http.scheme, net.peer.ip, net.peer.port, http.target
156+
String target = getTargetFromPeerService(attributes);
157+
if (target != null) {
158+
return target;
159+
}
160+
// note http.host includes the port (at least when non-default)
161+
target = attributes.get(SemanticAttributes.HTTP_HOST);
162+
if (target != null) {
163+
String scheme = attributes.get(SemanticAttributes.HTTP_SCHEME);
164+
if ("http".equals(scheme)) {
165+
if (target.endsWith(":80")) {
166+
target = target.substring(0, target.length() - 3);
167+
}
168+
} else if ("https".equals(scheme)) {
169+
if (target.endsWith(":443")) {
170+
target = target.substring(0, target.length() - 4);
171+
}
172+
}
173+
return target;
174+
}
175+
String url = attributes.get(SemanticAttributes.HTTP_URL);
176+
if (url != null) {
177+
target = getTargetFromUrl(url);
178+
if (target != null) {
179+
return target;
180+
}
181+
}
182+
String scheme = attributes.get(SemanticAttributes.HTTP_SCHEME);
183+
int defaultPort;
184+
if ("http".equals(scheme)) {
185+
defaultPort = 80;
186+
} else if ("https".equals(scheme)) {
187+
defaultPort = 443;
188+
} else {
189+
defaultPort = 0;
190+
}
191+
target = getTargetFromNetAttributes(attributes, defaultPort);
192+
if (target != null) {
193+
return target;
194+
}
195+
// this should not happen, just a failsafe
196+
return "Http";
197+
}
198+
199+
// this is copied from SpanDataMapper
200+
@Nullable
201+
private static String getTargetFromPeerService(Attributes attributes) {
202+
// do not append port to peer.service
203+
return attributes.get(SemanticAttributes.PEER_SERVICE);
204+
}
205+
206+
// this is copied from SpanDataMapper
207+
@Nullable
208+
private static String getTargetFromNetAttributes(Attributes attributes, int defaultPort) {
209+
String target = getHostFromNetAttributes(attributes);
210+
if (target == null) {
211+
return null;
212+
}
213+
// append net.peer.port to target
214+
Long port = attributes.get(SemanticAttributes.NET_PEER_PORT);
215+
if (port != null && port != defaultPort) {
216+
return target + ":" + port;
217+
}
218+
return target;
219+
}
220+
221+
// this is copied from SpanDataMapper
222+
@Nullable
223+
private static String getHostFromNetAttributes(Attributes attributes) {
224+
String host = attributes.get(SemanticAttributes.NET_PEER_NAME);
225+
if (host != null) {
226+
return host;
227+
}
228+
return attributes.get(SemanticAttributes.NET_PEER_IP);
229+
}
230+
231+
// this is copied from SpanDataMapper
232+
@Nullable
233+
private static String getTargetFromUrl(String url) {
234+
int schemeEndIndex = url.indexOf(':');
235+
if (schemeEndIndex == -1) {
236+
// not a valid url
237+
return null;
238+
}
239+
240+
int len = url.length();
241+
if (schemeEndIndex + 2 < len
242+
&& url.charAt(schemeEndIndex + 1) == '/'
243+
&& url.charAt(schemeEndIndex + 2) == '/') {
244+
// has authority component
245+
// look for
246+
// '/' - start of path
247+
// '?' or end of string - empty path
248+
int index;
249+
for (index = schemeEndIndex + 3; index < len; index++) {
250+
char c = url.charAt(index);
251+
if (c == '/' || c == '?' || c == '#') {
252+
break;
253+
}
254+
}
255+
String target = url.substring(schemeEndIndex + 3, index);
256+
return target.isEmpty() ? null : target;
257+
} else {
258+
// has no authority
259+
return null;
260+
}
261+
}
262+
263+
@Nullable
264+
private static <T> T getAttribute(AttributeKey<T> key, Attributes... attributesList) {
265+
for (Attributes attributes : attributesList) {
266+
T value = attributes.get(key);
267+
if (value != null) {
268+
return value;
269+
}
270+
}
271+
return null;
272+
}
273+
274+
@AutoValue
275+
abstract static class State {
276+
277+
abstract Attributes startAttributes();
278+
279+
abstract long startTimeNanos();
280+
}
281+
}

0 commit comments

Comments
 (0)