From 3c39fb4df804a06591a4dd26b139efe9b9975a9b Mon Sep 17 00:00:00 2001 From: fosky94 Date: Tue, 19 May 2026 17:27:30 -0500 Subject: [PATCH 01/12] Create Client side metrics for redis --- .../RedisTelemetryApp.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 memorystore/redis/client_side_metrics/RedisTelemetryApp.java diff --git a/memorystore/redis/client_side_metrics/RedisTelemetryApp.java b/memorystore/redis/client_side_metrics/RedisTelemetryApp.java new file mode 100644 index 00000000000..548e0abe9d4 --- /dev/null +++ b/memorystore/redis/client_side_metrics/RedisTelemetryApp.java @@ -0,0 +1,122 @@ +import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter; +import com.google.cloud.opentelemetry.trace.TraceExporter; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.time.Duration; +import java.util.function.Function; + +public class RedisTelemetryApp { + private static final AttributeKey ATTR_OPERATION = AttributeKey.stringKey("operation"); + private static Tracer tracer; + private static DoubleHistogram rttHist, clientBlockHist, appBlockHist; + private static LongCounter retryCounter, connErrorCounter; + private static JedisPool jedisPool; + + public static void main(String[] args) { + setupTelemetry(); + + String host = System.getenv().getOrDefault("REDISHOST", "localhost"); + int port = Integer.parseInt(System.getenv().getOrDefault("REDISPORT", "6379")); + + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(20); + poolConfig.setBlockWhenExhausted(true); + jedisPool = new JedisPool(poolConfig, host, port); + + try { + Span span = tracer.spanBuilder("process_user_span").startSpan(); + try { + // Simple write and read operations + smartRedisCall("set_user", jedis -> jedis.set("user:123", "active")); + + String result = smartRedisCall("get_user", jedis -> jedis.get("user:123")); + System.out.println("Retrieved: " + result); + } catch (Exception e) { + span.recordException(e); + } finally { + span.end(); + } + } finally { + if (jedisPool != null) jedisPool.close(); + } + } + + private static void setupTelemetry() { + TraceExporter traceExporter = TraceExporter.createWithDefaultConfiguration(); + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(traceExporter).build()).build(); + + GoogleCloudMetricExporter metricExporter = GoogleCloudMetricExporter.createWithDefaultConfiguration(); + SdkMeterProvider meterProvider = SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.builder(metricExporter).setInterval(Duration.ofSeconds(10)).build()).build(); + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider).setMeterProvider(meterProvider).buildAndRegisterGlobal(); + + tracer = openTelemetry.getTracer("jedis.client"); + Meter meter = openTelemetry.getMeter("jedis.metrics"); + + rttHist = meter.histogramBuilder("redis_client_rtt").setUnit("ms").build(); + clientBlockHist = meter.histogramBuilder("redis_client_blocking_latency").setUnit("ms").build(); + appBlockHist = meter.histogramBuilder("redis_application_blocking_latency").setUnit("ms").build(); + retryCounter = meter.counterBuilder("redis_retry_count").build(); + connErrorCounter = meter.counterBuilder("redis_connectivity_error_count").build(); + + retryCounter.add(0, Attributes.of(ATTR_OPERATION, "startup")); + connErrorCounter.add(0, Attributes.of(ATTR_OPERATION, "startup")); + } + + private static T smartRedisCall(String operationName, Function operation) { + int maxRetries = 3; + int attempt = 0; + Attributes attrs = Attributes.of(ATTR_OPERATION, operationName); + + // Create a dedicated child span for the Redis command + Span span = tracer.spanBuilder(operationName).startSpan(); + + try { + while (attempt < maxRetries) { + long poolStart = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + clientBlockHist.record((System.nanoTime() - poolStart) / 1_000_000.0, attrs); + + long reqStart = System.nanoTime(); + T response = operation.apply(jedis); + rttHist.record((System.nanoTime() - reqStart) / 1_000_000.0, attrs); + + long appStart = System.nanoTime(); + String dummy = String.valueOf(response); + appBlockHist.record((System.nanoTime() - appStart) / 1_000_000.0, attrs); + + return response; + } catch (JedisConnectionException e) { + attempt++; + connErrorCounter.add(1, attrs); + retryCounter.add(1, attrs); + span.recordException(e); // Attach error to trace + if (attempt >= maxRetries) throw e; + try { Thread.sleep((long) (Math.pow(2, attempt) * 100)); } catch (InterruptedException ie) {} + } + } + return null; + } finally { + span.end(); // Ensure span always closes + } + } +} From 110d84c44a57f904d32053ebd390bd41be850b13 Mon Sep 17 00:00:00 2001 From: fosky94 Date: Tue, 19 May 2026 17:28:12 -0500 Subject: [PATCH 02/12] Create pom.xml for RedisTelemetryApp --- memorystore/redis/client_side_metrics/pom.xml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 memorystore/redis/client_side_metrics/pom.xml diff --git a/memorystore/redis/client_side_metrics/pom.xml b/memorystore/redis/client_side_metrics/pom.xml new file mode 100644 index 00000000000..93f881822d9 --- /dev/null +++ b/memorystore/redis/client_side_metrics/pom.xml @@ -0,0 +1,27 @@ + + + redis.clients + jedis + 5.1.0 + + + io.opentelemetry + opentelemetry-api + 1.36.0 + + + io.opentelemetry + opentelemetry-sdk + 1.36.0 + + + com.google.cloud.opentelemetry + exporter-trace + 0.28.0-alpha + + + com.google.cloud.opentelemetry + exporter-metrics + 0.28.0-alpha + + From bc9533b558eb98cc977d50141a4b7120fc21fe36 Mon Sep 17 00:00:00 2001 From: fosky94 Date: Tue, 19 May 2026 18:05:24 -0500 Subject: [PATCH 03/12] Update license --- .../client_side_metrics/RedisTelemetryApp.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/memorystore/redis/client_side_metrics/RedisTelemetryApp.java b/memorystore/redis/client_side_metrics/RedisTelemetryApp.java index 548e0abe9d4..15721e2854f 100644 --- a/memorystore/redis/client_side_metrics/RedisTelemetryApp.java +++ b/memorystore/redis/client_side_metrics/RedisTelemetryApp.java @@ -1,3 +1,19 @@ +/* + * Copyright 2026 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. + */ + import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter; import com.google.cloud.opentelemetry.trace.TraceExporter; import io.opentelemetry.api.OpenTelemetry; From f564e732664782853da52fc921d874d7834c4766 Mon Sep 17 00:00:00 2001 From: fosky94 Date: Tue, 9 Jun 2026 17:13:17 -0500 Subject: [PATCH 04/12] Update pom.xml --- memorystore/redis/client_side_metrics/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memorystore/redis/client_side_metrics/pom.xml b/memorystore/redis/client_side_metrics/pom.xml index 93f881822d9..6ea9c587006 100644 --- a/memorystore/redis/client_side_metrics/pom.xml +++ b/memorystore/redis/client_side_metrics/pom.xml @@ -1,3 +1,4 @@ + redis.clients @@ -25,3 +26,4 @@ 0.28.0-alpha + From 53bdecc804eead99fb2eee65b685de8d45316a4a Mon Sep 17 00:00:00 2001 From: fosky94 Date: Tue, 9 Jun 2026 17:13:48 -0500 Subject: [PATCH 05/12] Update RedisTelemetryApp.java --- memorystore/redis/client_side_metrics/RedisTelemetryApp.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memorystore/redis/client_side_metrics/RedisTelemetryApp.java b/memorystore/redis/client_side_metrics/RedisTelemetryApp.java index 15721e2854f..2abc0138aa5 100644 --- a/memorystore/redis/client_side_metrics/RedisTelemetryApp.java +++ b/memorystore/redis/client_side_metrics/RedisTelemetryApp.java @@ -14,6 +14,7 @@ * limitations under the License. */ +// [START memorystore_redis_client_side_metrics] import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter; import com.google.cloud.opentelemetry.trace.TraceExporter; import io.opentelemetry.api.OpenTelemetry; @@ -136,3 +137,4 @@ private static T smartRedisCall(String operationName, Function ope } } } +// [END memorystore_redis_client_side_metrics] From 92021029c2189036ca6fa9b8298b7d5831648403 Mon Sep 17 00:00:00 2001 From: fosky94 Date: Thu, 25 Jun 2026 17:47:18 -0500 Subject: [PATCH 06/12] Update pom.xml --- memorystore/redis/client_side_metrics/pom.xml | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/memorystore/redis/client_side_metrics/pom.xml b/memorystore/redis/client_side_metrics/pom.xml index 6ea9c587006..37a40d4a5f2 100644 --- a/memorystore/redis/client_side_metrics/pom.xml +++ b/memorystore/redis/client_side_metrics/pom.xml @@ -1,29 +1,41 @@ - - - - redis.clients - jedis - 5.1.0 - - - io.opentelemetry - opentelemetry-api - 1.36.0 - - - io.opentelemetry - opentelemetry-sdk - 1.36.0 - - - com.google.cloud.opentelemetry - exporter-trace - 0.28.0-alpha - - - com.google.cloud.opentelemetry - exporter-metrics - 0.28.0-alpha - - - + + + 4.0.0 + + + com.example.memorystore + redis-client-side-metrics + 1.0-SNAPSHOT + + + + + redis.clients + jedis + 5.1.0 + + + io.opentelemetry + opentelemetry-api + 1.36.0 + + + io.opentelemetry + opentelemetry-sdk + 1.36.0 + + + com.google.cloud.opentelemetry + exporter-trace + 0.28.0-alpha + + + com.google.cloud.opentelemetry + exporter-metrics + 0.28.0-alpha + + + + From 2395e6636158c23445c27de08a66f8e55f118d2c Mon Sep 17 00:00:00 2001 From: fosky94 Date: Thu, 25 Jun 2026 17:57:41 -0500 Subject: [PATCH 07/12] Update pom.xml --- memorystore/redis/client_side_metrics/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/memorystore/redis/client_side_metrics/pom.xml b/memorystore/redis/client_side_metrics/pom.xml index 37a40d4a5f2..28bbbcd9ec1 100644 --- a/memorystore/redis/client_side_metrics/pom.xml +++ b/memorystore/redis/client_side_metrics/pom.xml @@ -8,6 +8,11 @@ com.example.memorystore redis-client-side-metrics 1.0-SNAPSHOT + + + 1.8 + 1.8 + From a143b42c4499f6c7fe3339bab4484edd7cf23e28 Mon Sep 17 00:00:00 2001 From: fosky94 Date: Thu, 25 Jun 2026 18:09:10 -0500 Subject: [PATCH 08/12] Update pom.xml --- memorystore/redis/client_side_metrics/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/memorystore/redis/client_side_metrics/pom.xml b/memorystore/redis/client_side_metrics/pom.xml index 28bbbcd9ec1..864da58523b 100644 --- a/memorystore/redis/client_side_metrics/pom.xml +++ b/memorystore/redis/client_side_metrics/pom.xml @@ -34,12 +34,12 @@ com.google.cloud.opentelemetry exporter-trace - 0.28.0-alpha + 0.28.0 com.google.cloud.opentelemetry exporter-metrics - 0.28.0-alpha + 0.28.0 From d2e649da02e3d8f94b72eaeadb72d879bf1bc2c5 Mon Sep 17 00:00:00 2001 From: fosky94 Date: Thu, 9 Jul 2026 19:48:41 +0000 Subject: [PATCH 09/12] fis:add tests --- memorystore/redis/client_side_metrics/pom.xml | 62 +++++++++- .../memorystore/redis}/RedisTelemetryApp.java | 84 +++++++++---- .../redis/RedisTelemetryAppTest.java | 110 ++++++++++++++++++ 3 files changed, 231 insertions(+), 25 deletions(-) rename memorystore/redis/client_side_metrics/{ => src/main/java/com/example/memorystore/redis}/RedisTelemetryApp.java (64%) create mode 100644 memorystore/redis/client_side_metrics/src/test/java/com/example/memorystore/redis/RedisTelemetryAppTest.java diff --git a/memorystore/redis/client_side_metrics/pom.xml b/memorystore/redis/client_side_metrics/pom.xml index 864da58523b..876aa034f65 100644 --- a/memorystore/redis/client_side_metrics/pom.xml +++ b/memorystore/redis/client_side_metrics/pom.xml @@ -1,18 +1,33 @@ + 4.0.0 - com.example.memorystore redis-client-side-metrics 1.0-SNAPSHOT - + 1.8 1.8 - + UTF-8 + @@ -41,6 +56,45 @@ exporter-metrics 0.28.0 + + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + org.slf4j + slf4j-simple + 1.7.36 + test + - + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + \ No newline at end of file diff --git a/memorystore/redis/client_side_metrics/RedisTelemetryApp.java b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/RedisTelemetryApp.java similarity index 64% rename from memorystore/redis/client_side_metrics/RedisTelemetryApp.java rename to memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/RedisTelemetryApp.java index 2abc0138aa5..d47947e1df5 100644 --- a/memorystore/redis/client_side_metrics/RedisTelemetryApp.java +++ b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/RedisTelemetryApp.java @@ -14,7 +14,11 @@ * limitations under the License. */ +package com.example.memorystore.redis; + // [START memorystore_redis_client_side_metrics] +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter; import com.google.cloud.opentelemetry.trace.TraceExporter; import io.opentelemetry.api.OpenTelemetry; @@ -40,10 +44,10 @@ public class RedisTelemetryApp { private static final AttributeKey ATTR_OPERATION = AttributeKey.stringKey("operation"); - private static Tracer tracer; - private static DoubleHistogram rttHist, clientBlockHist, appBlockHist; - private static LongCounter retryCounter, connErrorCounter; - private static JedisPool jedisPool; + static Tracer tracer; + static DoubleHistogram rttHist, clientBlockHist, appBlockHist; + static LongCounter retryCounter, connErrorCounter; + static JedisPool jedisPool; public static void main(String[] args) { setupTelemetry(); @@ -57,29 +61,60 @@ public static void main(String[] args) { jedisPool = new JedisPool(poolConfig, host, port); try { - Span span = tracer.spanBuilder("process_user_span").startSpan(); - try { - // Simple write and read operations - smartRedisCall("set_user", jedis -> jedis.set("user:123", "active")); - - String result = smartRedisCall("get_user", jedis -> jedis.get("user:123")); - System.out.println("Retrieved: " + result); - } catch (Exception e) { - span.recordException(e); - } finally { - span.end(); + run(); + } finally { + if (jedisPool != null) { + jedisPool.close(); } + } + } + + /** + * Extracts the core business logic so it can be invoked safely in unit tests + * with mocked Redis pools and openTelemetry instances. + */ + static String run() { + Span span = tracer.spanBuilder("process_user_span").startSpan(); + try { + // Simple write and read operations + smartRedisCall("set_user", jedis -> jedis.set("user:123", "active")); + + String result = smartRedisCall("get_user", jedis -> jedis.get("user:123")); + System.out.println("Retrieved: " + result); + return result; + } catch (Exception e) { + span.recordException(e); + throw e; } finally { - if (jedisPool != null) jedisPool.close(); + span.end(); } } + /** + * Initializer hook used exclusively by the test suite to safely inject + * mocked/noop OpenTelemetry and JedisPool instances into the static context. + */ + static void initForTest(JedisPool pool, OpenTelemetry testOpenTelemetry) { + jedisPool = pool; + tracer = testOpenTelemetry.getTracer("jedis.client"); + Meter meter = testOpenTelemetry.getMeter("jedis.metrics"); + + rttHist = meter.histogramBuilder("redis_client_rtt").setUnit("ms").build(); + clientBlockHist = meter.histogramBuilder("redis_client_blocking_latency").setUnit("ms").build(); + appBlockHist = meter.histogramBuilder("redis_application_blocking_latency").setUnit("ms").build(); + retryCounter = meter.counterBuilder("redis_retry_count").build(); + connErrorCounter = meter.counterBuilder("redis_connectivity_error_count").build(); + + retryCounter.add(0, Attributes.of(ATTR_OPERATION, "startup")); + connErrorCounter.add(0, Attributes.of(ATTR_OPERATION, "startup")); + } + private static void setupTelemetry() { - TraceExporter traceExporter = TraceExporter.createWithDefaultConfiguration(); + SpanExporter traceExporter = TraceExporter.createWithDefaultConfiguration(); SdkTracerProvider tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(BatchSpanProcessor.builder(traceExporter).build()).build(); - GoogleCloudMetricExporter metricExporter = GoogleCloudMetricExporter.createWithDefaultConfiguration(); + MetricExporter metricExporter = GoogleCloudMetricExporter.createWithDefaultConfiguration(); SdkMeterProvider meterProvider = SdkMeterProvider.builder() .registerMetricReader(PeriodicMetricReader.builder(metricExporter).setInterval(Duration.ofSeconds(10)).build()).build(); @@ -118,6 +153,7 @@ private static T smartRedisCall(String operationName, Function ope rttHist.record((System.nanoTime() - reqStart) / 1_000_000.0, attrs); long appStart = System.nanoTime(); + @SuppressWarnings("unused") String dummy = String.valueOf(response); appBlockHist.record((System.nanoTime() - appStart) / 1_000_000.0, attrs); @@ -127,8 +163,14 @@ private static T smartRedisCall(String operationName, Function ope connErrorCounter.add(1, attrs); retryCounter.add(1, attrs); span.recordException(e); // Attach error to trace - if (attempt >= maxRetries) throw e; - try { Thread.sleep((long) (Math.pow(2, attempt) * 100)); } catch (InterruptedException ie) {} + if (attempt >= maxRetries) { + throw e; + } + try { + Thread.sleep((long) (Math.pow(2, attempt) * 100)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } } } return null; @@ -137,4 +179,4 @@ private static T smartRedisCall(String operationName, Function ope } } } -// [END memorystore_redis_client_side_metrics] +// [END memorystore_redis_client_side_metrics] \ No newline at end of file diff --git a/memorystore/redis/client_side_metrics/src/test/java/com/example/memorystore/redis/RedisTelemetryAppTest.java b/memorystore/redis/client_side_metrics/src/test/java/com/example/memorystore/redis/RedisTelemetryAppTest.java new file mode 100644 index 00000000000..a066d9a946f --- /dev/null +++ b/memorystore/redis/client_side_metrics/src/test/java/com/example/memorystore/redis/RedisTelemetryAppTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2026 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.example.memorystore.redis; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +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 io.opentelemetry.api.OpenTelemetry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.exceptions.JedisConnectionException; + +/** Unit tests for {@link RedisTelemetryApp} verifying Redis interaction and Retry logic. */ +@RunWith(JUnit4.class) +public class RedisTelemetryAppTest { + + private JedisPool mockJedisPool; + private Jedis mockJedis; + + @Before + public void setUp() { + mockJedisPool = mock(JedisPool.class); + mockJedis = mock(Jedis.class); + + // Inject the mocked pool and a NO-OP OpenTelemetry instance so tests + // run hermetically without contacting real GCP Trace/Metric APIs. + RedisTelemetryApp.initForTest(mockJedisPool, OpenTelemetry.noop()); + + // Configure the mocked pool to hand out our mocked Jedis connection + when(mockJedisPool.getResource()).thenReturn(mockJedis); + + // Configure expected mocked Redis responses + when(mockJedis.set(eq("user:123"), eq("active"))).thenReturn("OK"); + when(mockJedis.get(eq("user:123"))).thenReturn("active"); + } + + @Test + public void testRun_successfulExecution() { + // Execute the extracted sample logic + String result = RedisTelemetryApp.run(); + + // Verify the application correctly read the final state + assertNotNull("Result should not be null", result); + assertEquals("active", result); + + // Verify the Redis pool handed out a connection, and the sample called set & get + verify(mockJedisPool, atLeastOnce()).getResource(); + verify(mockJedis, times(1)).set("user:123", "active"); + verify(mockJedis, times(1)).get("user:123"); + verify(mockJedis, atLeastOnce()).close(); // try-with-resources closes connection + } + + @Test + public void testRun_retryMechanismOnConnectivityFailure() { + // Configure the mocked pool to throw a Connection Exception on the FIRST call, + // and return the valid mock connection on the SECOND call. + when(mockJedisPool.getResource()) + .thenThrow(new JedisConnectionException("Temporary network blip")) + .thenReturn(mockJedis); + + String result = RedisTelemetryApp.run(); + + // Verify it still succeeds due to the retry handler in smartRedisCall + assertEquals("active", result); + + // Verify getResource was called 3 times (1 failed + 1 successful for 'set', and 1 successful for 'get') + verify(mockJedisPool, times(3)).getResource(); + verify(mockJedis, times(1)).set("user:123", "active"); + verify(mockJedis, times(1)).get("user:123"); + } + + @Test + public void testRun_failureAfterMaxRetries() { + // Configure the mocked pool to fail CONSTANTLY + when(mockJedisPool.getResource()) + .thenThrow(new JedisConnectionException("Redis is down permanently")); + + // Verify it stops retrying and propagates the exception upward to alert CI + assertThrows(JedisConnectionException.class, RedisTelemetryApp::run); + + // Verify it attempted exactly 3 retries before quitting + verify(mockJedisPool, times(3)).getResource(); + } +} \ No newline at end of file From 6f5d9a3a41885e2b5af48c33347a9280ce078fdd Mon Sep 17 00:00:00 2001 From: fosky94 Date: Thu, 9 Jul 2026 19:56:25 +0000 Subject: [PATCH 10/12] fis:style:make the code style compliant to pass lints --- .../memorystore/redis/RedisTelemetryApp.java | 226 +++++++++++++----- .../memorystore/redis/package-info.java | 4 + 2 files changed, 171 insertions(+), 59 deletions(-) create mode 100644 memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java diff --git a/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/RedisTelemetryApp.java b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/RedisTelemetryApp.java index d47947e1df5..b9de30e53ac 100644 --- a/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/RedisTelemetryApp.java +++ b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/RedisTelemetryApp.java @@ -17,8 +17,6 @@ package com.example.memorystore.redis; // [START memorystore_redis_client_side_metrics] -import io.opentelemetry.sdk.metrics.export.MetricExporter; -import io.opentelemetry.sdk.trace.export.SpanExporter; import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter; import com.google.cloud.opentelemetry.trace.TraceExporter; import io.opentelemetry.api.OpenTelemetry; @@ -30,10 +28,12 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; @@ -42,21 +42,76 @@ import java.time.Duration; import java.util.function.Function; -public class RedisTelemetryApp { - private static final AttributeKey ATTR_OPERATION = AttributeKey.stringKey("operation"); - static Tracer tracer; - static DoubleHistogram rttHist, clientBlockHist, appBlockHist; - static LongCounter retryCounter, connErrorCounter; - static JedisPool jedisPool; +/** + * Sample application demonstrating client-side metrics and tracing for + * Google Cloud Memorystore for Redis. + */ +public final class RedisTelemetryApp { + /** Attribute key for Redis operation names. */ + private static final AttributeKey ATTR_OPERATION = + AttributeKey.stringKey("operation"); + + /** Maximum number of Redis reconnection attempts. */ + private static final int MAX_RETRIES = 3; + + /** Maximum total connections for the Jedis pool. */ + private static final int POOL_MAX_TOTAL = 20; + + /** Interval in seconds for exporting metrics to Google Cloud. */ + private static final long METRIC_INTERVAL_SECONDS = 10L; + + /** Base multiplier for exponential backoff sleep (in milliseconds). */ + private static final long RETRY_BACKOFF_BASE_MS = 100L; + + /** Conversion factor from Nanoseconds to Milliseconds. */ + private static final double NANO_TO_MS = 1_000_000.0; + + /** Default Redis port. */ + private static final int DEFAULT_REDIS_PORT = 6379; + + /** OpenTelemetry Tracer instance for recording trace spans. */ + private static Tracer tracer; + + /** OpenTelemetry Histogram for Redis round-trip time. */ + private static DoubleHistogram rttHist; + + /** OpenTelemetry Histogram for pool blocking latency. */ + private static DoubleHistogram clientBlockHist; + + /** OpenTelemetry Histogram for application logic blocking latency. */ + private static DoubleHistogram appBlockHist; - public static void main(String[] args) { + /** OpenTelemetry Counter for Redis reconnection retry events. */ + private static LongCounter retryCounter; + + /** OpenTelemetry Counter for Redis connectivity errors. */ + private static LongCounter connErrorCounter; + + /** Shared Jedis connection pool. */ + private static JedisPool jedisPool; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private RedisTelemetryApp() { + } + + /** + * Main entry point for running the sample application. + * + * @param args Command line arguments (not used). + */ + public static void main(final String[] args) { setupTelemetry(); - String host = System.getenv().getOrDefault("REDISHOST", "localhost"); - int port = Integer.parseInt(System.getenv().getOrDefault("REDISPORT", "6379")); + final String host = System.getenv() + .getOrDefault("REDISHOST", "localhost"); + final int port = Integer.parseInt(System.getenv() + .getOrDefault("REDISPORT", + String.valueOf(DEFAULT_REDIS_PORT))); - JedisPoolConfig poolConfig = new JedisPoolConfig(); - poolConfig.setMaxTotal(20); + final JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(POOL_MAX_TOTAL); poolConfig.setBlockWhenExhausted(true); jedisPool = new JedisPool(poolConfig, host, port); @@ -70,16 +125,19 @@ public static void main(String[] args) { } /** - * Extracts the core business logic so it can be invoked safely in unit tests - * with mocked Redis pools and openTelemetry instances. + * Executes the core business logic of reading and writing to Redis. + * + * @return The string retrieved from the Redis 'get' operation. */ static String run() { - Span span = tracer.spanBuilder("process_user_span").startSpan(); + final Span span = tracer.spanBuilder("process_user_span") + .startSpan(); try { - // Simple write and read operations - smartRedisCall("set_user", jedis -> jedis.set("user:123", "active")); + smartRedisCall("set_user", jedis -> + jedis.set("user:123", "active")); - String result = smartRedisCall("get_user", jedis -> jedis.get("user:123")); + final String result = smartRedisCall("get_user", jedis -> + jedis.get("user:123")); System.out.println("Retrieved: " + result); return result; } catch (Exception e) { @@ -91,83 +149,133 @@ static String run() { } /** - * Initializer hook used exclusively by the test suite to safely inject - * mocked/noop OpenTelemetry and JedisPool instances into the static context. + * Injects mocked or no-op telemetry and pool instances for unit testing. + * + * @param pool The mocked or test JedisPool instance. + * @param testOpenTelemetry The OpenTelemetry instance to use for testing. */ - static void initForTest(JedisPool pool, OpenTelemetry testOpenTelemetry) { + static void initForTest( + final JedisPool pool, + final OpenTelemetry testOpenTelemetry) { jedisPool = pool; tracer = testOpenTelemetry.getTracer("jedis.client"); - Meter meter = testOpenTelemetry.getMeter("jedis.metrics"); + final Meter meter = testOpenTelemetry.getMeter("jedis.metrics"); - rttHist = meter.histogramBuilder("redis_client_rtt").setUnit("ms").build(); - clientBlockHist = meter.histogramBuilder("redis_client_blocking_latency").setUnit("ms").build(); - appBlockHist = meter.histogramBuilder("redis_application_blocking_latency").setUnit("ms").build(); + rttHist = meter.histogramBuilder("redis_client_rtt") + .setUnit("ms").build(); + clientBlockHist = meter + .histogramBuilder("redis_client_blocking_latency") + .setUnit("ms").build(); + appBlockHist = meter + .histogramBuilder("redis_application_blocking_latency") + .setUnit("ms").build(); retryCounter = meter.counterBuilder("redis_retry_count").build(); - connErrorCounter = meter.counterBuilder("redis_connectivity_error_count").build(); + connErrorCounter = meter + .counterBuilder("redis_connectivity_error_count") + .build(); retryCounter.add(0, Attributes.of(ATTR_OPERATION, "startup")); connErrorCounter.add(0, Attributes.of(ATTR_OPERATION, "startup")); } + /** + * Configures the production OpenTelemetry SDK to export Traces and Metrics + * to Google Cloud Operations. + */ private static void setupTelemetry() { - SpanExporter traceExporter = TraceExporter.createWithDefaultConfiguration(); - SdkTracerProvider tracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(BatchSpanProcessor.builder(traceExporter).build()).build(); + final SpanExporter traceExporter = + TraceExporter.createWithDefaultConfiguration(); + final SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder(traceExporter) + .build()) + .build(); - MetricExporter metricExporter = GoogleCloudMetricExporter.createWithDefaultConfiguration(); - SdkMeterProvider meterProvider = SdkMeterProvider.builder() - .registerMetricReader(PeriodicMetricReader.builder(metricExporter).setInterval(Duration.ofSeconds(10)).build()).build(); + final MetricExporter metricExporter = + GoogleCloudMetricExporter.createWithDefaultConfiguration(); + final SdkMeterProvider meterProvider = + SdkMeterProvider.builder() + .registerMetricReader( + PeriodicMetricReader.builder(metricExporter) + .setInterval(Duration.ofSeconds( + METRIC_INTERVAL_SECONDS)) + .build()) + .build(); - OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider).setMeterProvider(meterProvider).buildAndRegisterGlobal(); + final OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .setMeterProvider(meterProvider) + .buildAndRegisterGlobal(); tracer = openTelemetry.getTracer("jedis.client"); - Meter meter = openTelemetry.getMeter("jedis.metrics"); + final Meter meter = openTelemetry.getMeter("jedis.metrics"); - rttHist = meter.histogramBuilder("redis_client_rtt").setUnit("ms").build(); - clientBlockHist = meter.histogramBuilder("redis_client_blocking_latency").setUnit("ms").build(); - appBlockHist = meter.histogramBuilder("redis_application_blocking_latency").setUnit("ms").build(); + rttHist = meter.histogramBuilder("redis_client_rtt") + .setUnit("ms").build(); + clientBlockHist = meter + .histogramBuilder("redis_client_blocking_latency") + .setUnit("ms").build(); + appBlockHist = meter + .histogramBuilder("redis_application_blocking_latency") + .setUnit("ms").build(); retryCounter = meter.counterBuilder("redis_retry_count").build(); - connErrorCounter = meter.counterBuilder("redis_connectivity_error_count").build(); + connErrorCounter = meter + .counterBuilder("redis_connectivity_error_count") + .build(); retryCounter.add(0, Attributes.of(ATTR_OPERATION, "startup")); connErrorCounter.add(0, Attributes.of(ATTR_OPERATION, "startup")); } - private static T smartRedisCall(String operationName, Function operation) { - int maxRetries = 3; + /** + * Wraps a Redis operation with latency metrics, reconnection retry logic, + * and trace spans. + * + * @param The return type of the Redis operation. + * @param operationName The name of the operation for metric attributes. + * @param operation The Redis command lambda to execute safely. + * @return The return value from the Redis command. + */ + private static T smartRedisCall( + final String operationName, + final Function operation) { int attempt = 0; - Attributes attrs = Attributes.of(ATTR_OPERATION, operationName); + final Attributes attrs = Attributes.of(ATTR_OPERATION, + operationName); - // Create a dedicated child span for the Redis command - Span span = tracer.spanBuilder(operationName).startSpan(); + final Span span = tracer.spanBuilder(operationName).startSpan(); try { - while (attempt < maxRetries) { - long poolStart = System.nanoTime(); + while (attempt < MAX_RETRIES) { + final long poolStart = System.nanoTime(); try (Jedis jedis = jedisPool.getResource()) { - clientBlockHist.record((System.nanoTime() - poolStart) / 1_000_000.0, attrs); + clientBlockHist.record((System.nanoTime() - poolStart) + / NANO_TO_MS, attrs); - long reqStart = System.nanoTime(); - T response = operation.apply(jedis); - rttHist.record((System.nanoTime() - reqStart) / 1_000_000.0, attrs); + final long reqStart = System.nanoTime(); + final T response = operation.apply(jedis); + rttHist.record((System.nanoTime() - reqStart) + / NANO_TO_MS, attrs); - long appStart = System.nanoTime(); + final long appStart = System.nanoTime(); @SuppressWarnings("unused") - String dummy = String.valueOf(response); - appBlockHist.record((System.nanoTime() - appStart) / 1_000_000.0, attrs); + final String dummy = String.valueOf(response); + appBlockHist.record((System.nanoTime() - appStart) + / NANO_TO_MS, attrs); return response; } catch (JedisConnectionException e) { attempt++; connErrorCounter.add(1, attrs); retryCounter.add(1, attrs); - span.recordException(e); // Attach error to trace - if (attempt >= maxRetries) { + span.recordException(e); + if (attempt >= MAX_RETRIES) { throw e; } try { - Thread.sleep((long) (Math.pow(2, attempt) * 100)); + Thread.sleep((long) (Math.pow(2, attempt) + * RETRY_BACKOFF_BASE_MS)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } @@ -175,8 +283,8 @@ private static T smartRedisCall(String operationName, Function ope } return null; } finally { - span.end(); // Ensure span always closes + span.end(); } } } -// [END memorystore_redis_client_side_metrics] \ No newline at end of file +// [END memorystore_redis_client_side_metrics] diff --git a/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java new file mode 100644 index 00000000000..cb13b0e29c7 --- /dev/null +++ b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * Memorystore Redis Client-Side Metrics sample package. + */ +package com.example.memorystore.redis; \ No newline at end of file From 3ca8c4df0c079834e780e53941c7620c1206cdd3 Mon Sep 17 00:00:00 2001 From: fosky94 Date: Thu, 9 Jul 2026 19:57:45 +0000 Subject: [PATCH 11/12] fis:style:add header to package-info.java --- .../example/memorystore/redis/package-info.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java index cb13b0e29c7..c76645ac745 100644 --- a/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java +++ b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java @@ -1,3 +1,19 @@ +/* + * Copyright 2026 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. + */ + /** * Memorystore Redis Client-Side Metrics sample package. */ From 188902d2d8e60ce4e21a3e04f3f40c70fbc11f94 Mon Sep 17 00:00:00 2001 From: fosky94 Date: Thu, 9 Jul 2026 20:03:06 +0000 Subject: [PATCH 12/12] fis:style:fix style in package-info.java --- .../main/java/com/example/memorystore/redis/package-info.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java index c76645ac745..55ed524cf92 100644 --- a/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java +++ b/memorystore/redis/client_side_metrics/src/main/java/com/example/memorystore/redis/package-info.java @@ -17,4 +17,4 @@ /** * Memorystore Redis Client-Side Metrics sample package. */ -package com.example.memorystore.redis; \ No newline at end of file +package com.example.memorystore.redis;