Skip to content

Commit 5c291e8

Browse files
authored
feat: MetricsTracer implementation (#2421)
* feat: Opentelemetry implementation
1 parent 46b0a85 commit 5c291e8

File tree

3 files changed

+515
-2
lines changed

3 files changed

+515
-2
lines changed

gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java

+173-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@
3232

3333
import com.google.api.core.BetaApi;
3434
import com.google.api.core.InternalApi;
35+
import com.google.api.gax.rpc.ApiException;
36+
import com.google.api.gax.rpc.StatusCode;
37+
import com.google.common.annotations.VisibleForTesting;
38+
import com.google.common.base.Stopwatch;
39+
import java.util.HashMap;
40+
import java.util.Map;
41+
import java.util.concurrent.CancellationException;
42+
import java.util.concurrent.TimeUnit;
43+
import javax.annotation.Nullable;
44+
import org.threeten.bp.Duration;
3545

3646
/**
3747
* This class computes generic metrics that can be observed in the lifecycle of an RPC operation.
@@ -42,12 +52,173 @@
4252
@InternalApi
4353
public class MetricsTracer implements ApiTracer {
4454

45-
public MetricsTracer(MethodName methodName, MetricsRecorder metricsRecorder) {}
55+
private static final String STATUS_ATTRIBUTE = "status";
56+
57+
private Stopwatch attemptTimer;
58+
59+
private final Stopwatch operationTimer = Stopwatch.createStarted();
60+
61+
private final Map<String, String> attributes = new HashMap<>();
62+
63+
private MetricsRecorder metricsRecorder;
64+
65+
public MetricsTracer(MethodName methodName, MetricsRecorder metricsRecorder) {
66+
this.attributes.put("method_name", methodName.toString());
67+
this.metricsRecorder = metricsRecorder;
68+
}
69+
70+
/**
71+
* Signals that the overall operation has finished successfully. The tracer is now considered
72+
* closed and should no longer be used. Successful operation adds "OK" value to the status
73+
* attribute key.
74+
*/
75+
@Override
76+
public void operationSucceeded() {
77+
attributes.put(STATUS_ATTRIBUTE, StatusCode.Code.OK.toString());
78+
metricsRecorder.recordOperationLatency(
79+
operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
80+
metricsRecorder.recordOperationCount(1, attributes);
81+
}
82+
83+
/**
84+
* Signals that the operation was cancelled by the user. The tracer is now considered closed and
85+
* should no longer be used. Cancelled operation adds "CANCELLED" value to the status attribute
86+
* key.
87+
*/
88+
@Override
89+
public void operationCancelled() {
90+
attributes.put(STATUS_ATTRIBUTE, StatusCode.Code.CANCELLED.toString());
91+
metricsRecorder.recordOperationLatency(
92+
operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
93+
metricsRecorder.recordOperationCount(1, attributes);
94+
}
95+
96+
/**
97+
* Signals that the operation was cancelled by the user. The tracer is now considered closed and
98+
* should no longer be used. Failed operation extracts the error from the throwable and adds it to
99+
* the status attribute key.
100+
*/
101+
@Override
102+
public void operationFailed(Throwable error) {
103+
attributes.put(STATUS_ATTRIBUTE, extractStatus(error));
104+
metricsRecorder.recordOperationLatency(
105+
operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
106+
metricsRecorder.recordOperationCount(1, attributes);
107+
}
108+
109+
/**
110+
* Adds an annotation that an attempt is about to start with additional information from the
111+
* request. In general this should occur at the very start of the operation. The attemptNumber is
112+
* zero based. So the initial attempt will be 0. When the attempt starts, the attemptTimer starts
113+
* the stopwatch.
114+
*
115+
* @param attemptNumber the zero based sequential attempt number.
116+
* @param request request of this attempt.
117+
*/
118+
@Override
119+
public void attemptStarted(Object request, int attemptNumber) {
120+
attemptTimer = Stopwatch.createStarted();
121+
}
122+
123+
/**
124+
* Adds an annotation that the attempt succeeded. Successful attempt add "OK" value to the status
125+
* attribute key.
126+
*/
127+
@Override
128+
public void attemptSucceeded() {
129+
130+
attributes.put(STATUS_ATTRIBUTE, StatusCode.Code.OK.toString());
131+
metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
132+
metricsRecorder.recordAttemptCount(1, attributes);
133+
}
134+
135+
/**
136+
* Add an annotation that the attempt was cancelled by the user. Cancelled attempt add "CANCELLED"
137+
* to the status attribute key.
138+
*/
139+
@Override
140+
public void attemptCancelled() {
141+
142+
attributes.put(STATUS_ATTRIBUTE, StatusCode.Code.CANCELLED.toString());
143+
metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
144+
metricsRecorder.recordAttemptCount(1, attributes);
145+
}
146+
147+
/**
148+
* Adds an annotation that the attempt failed, but another attempt will be made after the delay.
149+
*
150+
* @param error the error that caused the attempt to fail.
151+
* @param delay the amount of time to wait before the next attempt will start.
152+
* <p>Failed attempt extracts the error from the throwable and adds it to the status attribute
153+
* key.
154+
*/
155+
@Override
156+
public void attemptFailed(Throwable error, Duration delay) {
157+
158+
attributes.put(STATUS_ATTRIBUTE, extractStatus(error));
159+
metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
160+
metricsRecorder.recordAttemptCount(1, attributes);
161+
}
162+
163+
/**
164+
* Adds an annotation that the attempt failed and that no further attempts will be made because
165+
* retry limits have been reached. This extracts the error from the throwable and adds it to the
166+
* status attribute key.
167+
*
168+
* @param error the last error received before retries were exhausted.
169+
*/
170+
@Override
171+
public void attemptFailedRetriesExhausted(Throwable error) {
172+
173+
attributes.put(STATUS_ATTRIBUTE, extractStatus(error));
174+
metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
175+
metricsRecorder.recordAttemptCount(1, attributes);
176+
}
177+
178+
/**
179+
* Adds an annotation that the attempt failed and that no further attempts will be made because
180+
* the last error was not retryable. This extracts the error from the throwable and adds it to the
181+
* status attribute key.
182+
*
183+
* @param error the error that caused the final attempt to fail.
184+
*/
185+
@Override
186+
public void attemptPermanentFailure(Throwable error) {
187+
188+
attributes.put(STATUS_ATTRIBUTE, extractStatus(error));
189+
metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
190+
metricsRecorder.recordAttemptCount(1, attributes);
191+
}
192+
193+
/** Function to extract the status of the error as a string */
194+
@VisibleForTesting
195+
static String extractStatus(@Nullable Throwable error) {
196+
final String statusString;
197+
198+
if (error == null) {
199+
return StatusCode.Code.OK.toString();
200+
} else if (error instanceof CancellationException) {
201+
statusString = StatusCode.Code.CANCELLED.toString();
202+
} else if (error instanceof ApiException) {
203+
statusString = ((ApiException) error).getStatusCode().getCode().toString();
204+
} else {
205+
statusString = StatusCode.Code.UNKNOWN.toString();
206+
}
207+
208+
return statusString;
209+
}
46210

47211
/**
48212
* Add attributes that will be attached to all metrics. This is expected to be called by
49213
* handwritten client teams to add additional attributes that are not supposed be collected by
50214
* Gax.
51215
*/
52-
public void addAttributes(String key, String value) {};
216+
public void addAttributes(String key, String value) {
217+
attributes.put(key, value);
218+
};
219+
220+
@VisibleForTesting
221+
Map<String, String> getAttributes() {
222+
return attributes;
223+
}
53224
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.tracing;
31+
32+
import static org.mockito.Mockito.mock;
33+
import static org.mockito.Mockito.when;
34+
35+
import com.google.api.gax.tracing.ApiTracerFactory.OperationType;
36+
import com.google.common.truth.Truth;
37+
import org.junit.Before;
38+
import org.junit.Test;
39+
import org.mockito.Mock;
40+
41+
public class MetricsTracerFactoryTest {
42+
@Mock private MetricsRecorder metricsRecorder;
43+
@Mock private ApiTracer parent;
44+
private SpanName spanName;
45+
private MetricsTracerFactory metricsTracerFactory;
46+
47+
@Before
48+
public void setUp() {
49+
// Create an instance of MetricsTracerFactory with the mocked MetricsRecorder
50+
metricsTracerFactory = new MetricsTracerFactory(metricsRecorder);
51+
52+
spanName = mock(SpanName.class);
53+
when(spanName.getClientName()).thenReturn("testService");
54+
when(spanName.getMethodName()).thenReturn("testMethod");
55+
}
56+
57+
@Test
58+
public void testNewTracer_notNull() {
59+
// Call the newTracer method
60+
ApiTracer apiTracer = metricsTracerFactory.newTracer(parent, spanName, OperationType.Unary);
61+
62+
// Assert that the apiTracer created has expected type and not null
63+
Truth.assertThat(apiTracer).isInstanceOf(MetricsTracer.class);
64+
Truth.assertThat(apiTracer).isNotNull();
65+
}
66+
67+
@Test
68+
public void testNewTracer_HasCorrectParameters() {
69+
70+
// Call the newTracer method
71+
ApiTracer apiTracer = metricsTracerFactory.newTracer(parent, spanName, OperationType.Unary);
72+
73+
// Assert that the apiTracer created has expected type and not null
74+
Truth.assertThat(apiTracer).isInstanceOf(MetricsTracer.class);
75+
Truth.assertThat(apiTracer).isNotNull();
76+
77+
MetricsTracer metricsTracer = (MetricsTracer) apiTracer;
78+
Truth.assertThat(metricsTracer.getAttributes().get("method_name"))
79+
.isEqualTo("testService.testMethod");
80+
}
81+
}

0 commit comments

Comments
 (0)