From e00b88449a8642fa0db920019cbb06b1d6d2e2f3 Mon Sep 17 00:00:00 2001 From: surbhigarg92 Date: Tue, 18 Jun 2024 12:40:20 +0000 Subject: [PATCH] chore: Composite Tracer (#3163) --- README.md | 8 +- .../google/cloud/spanner/CompositeTracer.java | 174 +++++++++++ .../cloud/spanner/CompositeTracerFactory.java | 46 +++ .../google/cloud/spanner/SpannerOptions.java | 8 +- .../cloud/spanner/CompositeTracerTest.java | 277 ++++++++++++++++++ 5 files changed, 508 insertions(+), 5 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/CompositeTracer.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/CompositeTracerFactory.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/CompositeTracerTest.java diff --git a/README.md b/README.md index ce82a63f83c..ab72fa7ae3d 100644 --- a/README.md +++ b/README.md @@ -50,20 +50,20 @@ If you are using Maven without the BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies: ```Groovy -implementation platform('com.google.cloud:libraries-bom:26.40.0') +implementation platform('com.google.cloud:libraries-bom:26.41.0') implementation 'com.google.cloud:google-cloud-spanner' ``` If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-spanner:6.68.1' +implementation 'com.google.cloud:google-cloud-spanner:6.69.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.68.1" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.69.0" ``` @@ -687,7 +687,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.68.1 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.69.0 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CompositeTracer.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CompositeTracer.java new file mode 100644 index 00000000000..eed687c416d --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CompositeTracer.java @@ -0,0 +1,174 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.InternalApi; +import com.google.api.gax.tracing.ApiTracer; +import com.google.api.gax.tracing.BaseApiTracer; +import com.google.api.gax.tracing.MetricsTracer; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import org.threeten.bp.Duration; + +@InternalApi +public class CompositeTracer extends BaseApiTracer { + private final List children; + + public CompositeTracer(List children) { + this.children = ImmutableList.copyOf(children); + } + + @Override + public Scope inScope() { + final List childScopes = new ArrayList<>(children.size()); + + for (ApiTracer child : children) { + childScopes.add(child.inScope()); + } + + return new Scope() { + @Override + public void close() { + for (Scope childScope : childScopes) { + childScope.close(); + } + } + }; + } + + @Override + public void operationSucceeded() { + for (ApiTracer child : children) { + child.operationSucceeded(); + } + } + + @Override + public void operationCancelled() { + for (ApiTracer child : children) { + child.operationCancelled(); + } + } + + @Override + public void operationFailed(Throwable error) { + for (ApiTracer child : children) { + child.operationFailed(error); + } + } + + @Override + public void connectionSelected(String id) { + for (ApiTracer child : children) { + child.connectionSelected(id); + } + } + + @Override + public void attemptStarted(int attemptNumber) { + for (ApiTracer child : children) { + child.attemptStarted(null, attemptNumber); + } + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + for (ApiTracer child : children) { + child.attemptStarted(request, attemptNumber); + } + } + + @Override + public void attemptSucceeded() { + for (ApiTracer child : children) { + child.attemptSucceeded(); + } + } + + @Override + public void attemptCancelled() { + for (ApiTracer child : children) { + child.attemptCancelled(); + } + } + + @Override + public void attemptFailed(Throwable error, Duration delay) { + for (ApiTracer child : children) { + child.attemptFailed(error, delay); + } + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + for (ApiTracer child : children) { + child.attemptFailedRetriesExhausted(error); + } + } + + @Override + public void attemptPermanentFailure(Throwable error) { + for (ApiTracer child : children) { + child.attemptPermanentFailure(error); + } + } + + @Override + public void lroStartFailed(Throwable error) { + for (ApiTracer child : children) { + child.lroStartFailed(error); + } + } + + @Override + public void lroStartSucceeded() { + for (ApiTracer child : children) { + child.lroStartSucceeded(); + } + } + + @Override + public void responseReceived() { + for (ApiTracer child : children) { + child.responseReceived(); + } + } + + @Override + public void requestSent() { + for (ApiTracer child : children) { + child.requestSent(); + } + } + + @Override + public void batchRequestSent(long elementCount, long requestSize) { + for (ApiTracer child : children) { + child.batchRequestSent(elementCount, requestSize); + } + } + + public void addAttributes(String key, String value) { + for (ApiTracer child : children) { + if (child instanceof MetricsTracer) { + MetricsTracer metricsTracer = (MetricsTracer) child; + metricsTracer.addAttributes(key, value); + } + } + }; +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CompositeTracerFactory.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CompositeTracerFactory.java new file mode 100644 index 00000000000..2e3965de095 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CompositeTracerFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.InternalApi; +import com.google.api.gax.tracing.ApiTracer; +import com.google.api.gax.tracing.ApiTracerFactory; +import com.google.api.gax.tracing.BaseApiTracerFactory; +import com.google.api.gax.tracing.SpanName; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; + +@InternalApi +public class CompositeTracerFactory extends BaseApiTracerFactory { + + private final List apiTracerFactories; + + public CompositeTracerFactory(List apiTracerFactories) { + this.apiTracerFactories = ImmutableList.copyOf(apiTracerFactories); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + List children = new ArrayList<>(apiTracerFactories.size()); + + for (ApiTracerFactory factory : apiTracerFactories) { + children.add(factory.newTracer(parent, spanName, operationType)); + } + return new CompositeTracer(children); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 79e04805943..c3e75a7ed54 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -72,7 +72,9 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -1621,8 +1623,12 @@ public OpenTelemetry getOpenTelemetry() { @Override public ApiTracerFactory getApiTracerFactory() { + List apiTracerFactories = new ArrayList(); // Prefer any direct ApiTracerFactory that might have been set on the builder. - return MoreObjects.firstNonNull(super.getApiTracerFactory(), getDefaultApiTracerFactory()); + apiTracerFactories.add( + MoreObjects.firstNonNull(super.getApiTracerFactory(), getDefaultApiTracerFactory())); + + return new CompositeTracerFactory(apiTracerFactories); } private ApiTracerFactory getDefaultApiTracerFactory() { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/CompositeTracerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/CompositeTracerTest.java new file mode 100644 index 00000000000..dfb7b252268 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/CompositeTracerTest.java @@ -0,0 +1,277 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.gax.tracing.ApiTracer; +import com.google.api.gax.tracing.ApiTracer.Scope; +import com.google.api.gax.tracing.MetricsTracer; +import com.google.common.collect.ImmutableList; +import com.google.spanner.v1.ReadRequest; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.threeten.bp.Duration; + +@RunWith(JUnit4.class) +public class CompositeTracerTest { + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock private ApiTracer child1; + @Mock private ApiTracer child2; + @Mock private OpenTelemetryApiTracer child3; + @Mock private MetricsTracer child4; + + private CompositeTracer compositeTracer; + + @Before + public void setup() { + compositeTracer = new CompositeTracer(ImmutableList.of(child1, child2, child3, child4)); + } + + @Test + public void testInScope() { + Scope scope1 = mock(Scope.class); + when(child1.inScope()).thenReturn(scope1); + + Scope scope2 = mock(Scope.class); + when(child2.inScope()).thenReturn(scope2); + + Scope scope3 = mock(Scope.class); + when(child3.inScope()).thenReturn(scope3); + + Scope scope4 = mock(Scope.class); + when(child4.inScope()).thenReturn(scope4); + + Scope parentScope = compositeTracer.inScope(); + + parentScope.close(); + verify(scope1, times(1)).close(); + verify(scope2, times(1)).close(); + verify(scope3, times(1)).close(); + verify(scope4, times(1)).close(); + } + + @Test + public void testOperationSucceeded() { + compositeTracer.operationSucceeded(); + verify(child1, times(1)).operationSucceeded(); + verify(child2, times(1)).operationSucceeded(); + verify(child3, times(1)).operationSucceeded(); + verify(child4, times(1)).operationSucceeded(); + } + + @Test + public void testOperationCancelled() { + compositeTracer.operationCancelled(); + verify(child1, times(1)).operationCancelled(); + verify(child2, times(1)).operationCancelled(); + verify(child3, times(1)).operationCancelled(); + verify(child4, times(1)).operationCancelled(); + } + + @Test + public void testOperationFailed() { + RuntimeException error = new RuntimeException(); + compositeTracer.operationFailed(error); + verify(child1, times(1)).operationFailed(error); + verify(child2, times(1)).operationFailed(error); + verify(child3, times(1)).operationFailed(error); + verify(child4, times(1)).operationFailed(error); + } + + @Test + public void testConnectionSelected() { + compositeTracer.connectionSelected("connection-one"); + verify(child1, times(1)).connectionSelected("connection-one"); + verify(child2, times(1)).connectionSelected("connection-one"); + verify(child3, times(1)).connectionSelected("connection-one"); + verify(child4, times(1)).connectionSelected("connection-one"); + } + + @Test + public void testAttemptStarted() { + ReadRequest request = ReadRequest.getDefaultInstance(); + compositeTracer.attemptStarted(request, 3); + verify(child1, times(1)).attemptStarted(request, 3); + verify(child2, times(1)).attemptStarted(request, 3); + verify(child3, times(1)).attemptStarted(request, 3); + verify(child4, times(1)).attemptStarted(request, 3); + } + + @Test + public void testAttemptSucceeded() { + compositeTracer.attemptSucceeded(); + verify(child1, times(1)).attemptSucceeded(); + verify(child2, times(1)).attemptSucceeded(); + verify(child3, times(1)).attemptSucceeded(); + verify(child4, times(1)).attemptSucceeded(); + } + + @Test + public void testAttemptCancelled() { + compositeTracer.attemptCancelled(); + verify(child1, times(1)).attemptCancelled(); + verify(child2, times(1)).attemptCancelled(); + verify(child3, times(1)).attemptCancelled(); + verify(child4, times(1)).attemptCancelled(); + } + + @Test + public void testAttemptFailed() { + RuntimeException error = new RuntimeException(); + Duration delay = Duration.ofMillis(10); + compositeTracer.attemptFailed(error, delay); + verify(child1, times(1)).attemptFailed(error, delay); + verify(child2, times(1)).attemptFailed(error, delay); + verify(child3, times(1)).attemptFailed(error, delay); + verify(child4, times(1)).attemptFailed(error, delay); + } + + @Test + public void testAttemptFailedRetriesExhausted() { + RuntimeException error = new RuntimeException(); + compositeTracer.attemptFailedRetriesExhausted(error); + verify(child1, times(1)).attemptFailedRetriesExhausted(error); + verify(child2, times(1)).attemptFailedRetriesExhausted(error); + verify(child3, times(1)).attemptFailedRetriesExhausted(error); + verify(child4, times(1)).attemptFailedRetriesExhausted(error); + } + + @Test + public void testAttemptPermanentFailure() { + RuntimeException error = new RuntimeException(); + compositeTracer.attemptPermanentFailure(error); + verify(child1, times(1)).attemptPermanentFailure(error); + verify(child2, times(1)).attemptPermanentFailure(error); + verify(child3, times(1)).attemptPermanentFailure(error); + verify(child4, times(1)).attemptPermanentFailure(error); + } + + @Test + public void testLroStartFailed() { + RuntimeException error = new RuntimeException(); + compositeTracer.lroStartFailed(error); + verify(child1, times(1)).lroStartFailed(error); + verify(child2, times(1)).lroStartFailed(error); + verify(child3, times(1)).lroStartFailed(error); + verify(child4, times(1)).lroStartFailed(error); + } + + @Test + public void testLroStartSucceeded() { + compositeTracer.lroStartSucceeded(); + verify(child1, times(1)).lroStartSucceeded(); + verify(child2, times(1)).lroStartSucceeded(); + verify(child3, times(1)).lroStartSucceeded(); + verify(child4, times(1)).lroStartSucceeded(); + } + + @Test + public void testResponseReceived() { + compositeTracer.responseReceived(); + verify(child1, times(1)).responseReceived(); + verify(child2, times(1)).responseReceived(); + verify(child3, times(1)).responseReceived(); + verify(child4, times(1)).responseReceived(); + } + + @Test + public void testRequestSent() { + compositeTracer.requestSent(); + verify(child1, times(1)).requestSent(); + verify(child2, times(1)).requestSent(); + verify(child3, times(1)).requestSent(); + verify(child4, times(1)).requestSent(); + } + + @Test + public void testBatchRequestSent() { + compositeTracer.batchRequestSent(2, 20); + verify(child1, times(1)).batchRequestSent(2, 20); + verify(child2, times(1)).batchRequestSent(2, 20); + verify(child3, times(1)).batchRequestSent(2, 20); + verify(child4, times(1)).batchRequestSent(2, 20); + } + + @Test + public void testMethodsOverrideMetricsTracer() { + Method[] metricsTracerMethods = MetricsTracer.class.getDeclaredMethods(); + Method[] compositeTracerMethods = CompositeTracer.class.getDeclaredMethods(); + + List visibleForTestingMethods = Arrays.asList("getAttributes", "extractStatus"); + + Set compositeMethodsSet = new HashSet<>(Arrays.asList(compositeTracerMethods)); + + for (Method metricsMethod : metricsTracerMethods) { + if (!visibleForTestingMethods.contains(metricsMethod.getName()) + && !containsMethod(compositeMethodsSet, metricsMethod)) { + throw new AssertionError("Method not found in compositeTracerMethods: " + metricsMethod); + } + } + } + + @Test + public void testMethodsOverrideOpenTelemetryTracer() { + + Method[] compositeTracerMethods = CompositeTracer.class.getDeclaredMethods(); + + List openTelemetryTracerMethods = + Arrays.stream(OpenTelemetryApiTracer.class.getDeclaredMethods()) + .filter(method -> java.lang.reflect.Modifier.isPublic(method.getModifiers())) + .collect(Collectors.toList()); + + Set compositeMethodsSet = new HashSet<>(Arrays.asList(compositeTracerMethods)); + + for (Method metricsMethod : openTelemetryTracerMethods) { + if (!containsMethod(compositeMethodsSet, metricsMethod)) { + throw new AssertionError("Method not found in compositeTracerMethods: " + metricsMethod); + } + } + } + + private boolean compareMethods(Method actual, Method expected) { + return actual.getName().equals(expected.getName()) + && Arrays.equals(actual.getParameterTypes(), expected.getParameterTypes()) + && actual.getModifiers() == expected.getModifiers() + && actual.getReturnType().equals(expected.getReturnType()); + } + + public boolean containsMethod(Set methodSet, Method method) { + for (Method m : methodSet) { + if (compareMethods(m, method)) { + return true; + } + } + return false; + } +}