Skip to content

Commit 4225760

Browse files
authored
Create Client side metrics for redis (#10271)
* Create Client side metrics for redis * Create pom.xml for RedisTelemetryApp * Update license * Update pom.xml * Update RedisTelemetryApp.java * Update pom.xml * Update pom.xml * Update pom.xml * fis:add tests * fis:style:make the code style compliant to pass lints * fis:style:add header to package-info.java * fis:style:fix style in package-info.java
1 parent 58cf289 commit 4225760

4 files changed

Lines changed: 520 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright 2026 Google LLC
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<project xmlns="http://maven.apache.org/POM/4.0.0"
18+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
19+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
20+
<modelVersion>4.0.0</modelVersion>
21+
22+
<groupId>com.example.memorystore</groupId>
23+
<artifactId>redis-client-side-metrics</artifactId>
24+
<version>1.0-SNAPSHOT</version>
25+
26+
<properties>
27+
<maven.compiler.source>1.8</maven.compiler.source>
28+
<maven.compiler.target>1.8</maven.compiler.target>
29+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
30+
</properties>
31+
32+
<!-- [START memorystore_redis_client_side_metrics_dependencies] -->
33+
<dependencies>
34+
<dependency>
35+
<groupId>redis.clients</groupId>
36+
<artifactId>jedis</artifactId>
37+
<version>5.1.0</version>
38+
</dependency>
39+
<dependency>
40+
<groupId>io.opentelemetry</groupId>
41+
<artifactId>opentelemetry-api</artifactId>
42+
<version>1.36.0</version>
43+
</dependency>
44+
<dependency>
45+
<groupId>io.opentelemetry</groupId>
46+
<artifactId>opentelemetry-sdk</artifactId>
47+
<version>1.36.0</version>
48+
</dependency>
49+
<dependency>
50+
<groupId>com.google.cloud.opentelemetry</groupId>
51+
<artifactId>exporter-trace</artifactId>
52+
<version>0.28.0</version>
53+
</dependency>
54+
<dependency>
55+
<groupId>com.google.cloud.opentelemetry</groupId>
56+
<artifactId>exporter-metrics</artifactId>
57+
<version>0.28.0</version>
58+
</dependency>
59+
60+
<!-- Testing Dependencies -->
61+
<dependency>
62+
<groupId>junit</groupId>
63+
<artifactId>junit</artifactId>
64+
<version>4.13.2</version>
65+
<scope>test</scope>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.mockito</groupId>
69+
<artifactId>mockito-core</artifactId>
70+
<version>4.11.0</version>
71+
<scope>test</scope>
72+
</dependency>
73+
<dependency>
74+
<groupId>org.slf4j</groupId>
75+
<artifactId>slf4j-simple</artifactId>
76+
<version>1.7.36</version>
77+
<scope>test</scope>
78+
</dependency>
79+
</dependencies>
80+
<!-- [END memorystore_redis_client_side_metrics_dependencies] -->
81+
82+
<build>
83+
<plugins>
84+
<plugin>
85+
<groupId>org.apache.maven.plugins</groupId>
86+
<artifactId>maven-compiler-plugin</artifactId>
87+
<version>3.12.1</version>
88+
<configuration>
89+
<source>1.8</source>
90+
<target>1.8</target>
91+
</configuration>
92+
</plugin>
93+
<plugin>
94+
<groupId>org.apache.maven.plugins</groupId>
95+
<artifactId>maven-surefire-plugin</artifactId>
96+
<version>3.2.5</version>
97+
</plugin>
98+
</plugins>
99+
</build>
100+
</project>
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.memorystore.redis;
18+
19+
// [START memorystore_redis_client_side_metrics]
20+
import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter;
21+
import com.google.cloud.opentelemetry.trace.TraceExporter;
22+
import io.opentelemetry.api.OpenTelemetry;
23+
import io.opentelemetry.api.common.AttributeKey;
24+
import io.opentelemetry.api.common.Attributes;
25+
import io.opentelemetry.api.metrics.DoubleHistogram;
26+
import io.opentelemetry.api.metrics.LongCounter;
27+
import io.opentelemetry.api.metrics.Meter;
28+
import io.opentelemetry.api.trace.Span;
29+
import io.opentelemetry.api.trace.Tracer;
30+
import io.opentelemetry.sdk.OpenTelemetrySdk;
31+
import io.opentelemetry.sdk.metrics.export.MetricExporter;
32+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
33+
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
34+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
35+
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
36+
import io.opentelemetry.sdk.trace.export.SpanExporter;
37+
import redis.clients.jedis.Jedis;
38+
import redis.clients.jedis.JedisPool;
39+
import redis.clients.jedis.JedisPoolConfig;
40+
import redis.clients.jedis.exceptions.JedisConnectionException;
41+
42+
import java.time.Duration;
43+
import java.util.function.Function;
44+
45+
/**
46+
* Sample application demonstrating client-side metrics and tracing for
47+
* Google Cloud Memorystore for Redis.
48+
*/
49+
public final class RedisTelemetryApp {
50+
/** Attribute key for Redis operation names. */
51+
private static final AttributeKey<String> ATTR_OPERATION =
52+
AttributeKey.stringKey("operation");
53+
54+
/** Maximum number of Redis reconnection attempts. */
55+
private static final int MAX_RETRIES = 3;
56+
57+
/** Maximum total connections for the Jedis pool. */
58+
private static final int POOL_MAX_TOTAL = 20;
59+
60+
/** Interval in seconds for exporting metrics to Google Cloud. */
61+
private static final long METRIC_INTERVAL_SECONDS = 10L;
62+
63+
/** Base multiplier for exponential backoff sleep (in milliseconds). */
64+
private static final long RETRY_BACKOFF_BASE_MS = 100L;
65+
66+
/** Conversion factor from Nanoseconds to Milliseconds. */
67+
private static final double NANO_TO_MS = 1_000_000.0;
68+
69+
/** Default Redis port. */
70+
private static final int DEFAULT_REDIS_PORT = 6379;
71+
72+
/** OpenTelemetry Tracer instance for recording trace spans. */
73+
private static Tracer tracer;
74+
75+
/** OpenTelemetry Histogram for Redis round-trip time. */
76+
private static DoubleHistogram rttHist;
77+
78+
/** OpenTelemetry Histogram for pool blocking latency. */
79+
private static DoubleHistogram clientBlockHist;
80+
81+
/** OpenTelemetry Histogram for application logic blocking latency. */
82+
private static DoubleHistogram appBlockHist;
83+
84+
/** OpenTelemetry Counter for Redis reconnection retry events. */
85+
private static LongCounter retryCounter;
86+
87+
/** OpenTelemetry Counter for Redis connectivity errors. */
88+
private static LongCounter connErrorCounter;
89+
90+
/** Shared Jedis connection pool. */
91+
private static JedisPool jedisPool;
92+
93+
/**
94+
* Private constructor to prevent instantiation of this utility class.
95+
*/
96+
private RedisTelemetryApp() {
97+
}
98+
99+
/**
100+
* Main entry point for running the sample application.
101+
*
102+
* @param args Command line arguments (not used).
103+
*/
104+
public static void main(final String[] args) {
105+
setupTelemetry();
106+
107+
final String host = System.getenv()
108+
.getOrDefault("REDISHOST", "localhost");
109+
final int port = Integer.parseInt(System.getenv()
110+
.getOrDefault("REDISPORT",
111+
String.valueOf(DEFAULT_REDIS_PORT)));
112+
113+
final JedisPoolConfig poolConfig = new JedisPoolConfig();
114+
poolConfig.setMaxTotal(POOL_MAX_TOTAL);
115+
poolConfig.setBlockWhenExhausted(true);
116+
jedisPool = new JedisPool(poolConfig, host, port);
117+
118+
try {
119+
run();
120+
} finally {
121+
if (jedisPool != null) {
122+
jedisPool.close();
123+
}
124+
}
125+
}
126+
127+
/**
128+
* Executes the core business logic of reading and writing to Redis.
129+
*
130+
* @return The string retrieved from the Redis 'get' operation.
131+
*/
132+
static String run() {
133+
final Span span = tracer.spanBuilder("process_user_span")
134+
.startSpan();
135+
try {
136+
smartRedisCall("set_user", jedis ->
137+
jedis.set("user:123", "active"));
138+
139+
final String result = smartRedisCall("get_user", jedis ->
140+
jedis.get("user:123"));
141+
System.out.println("Retrieved: " + result);
142+
return result;
143+
} catch (Exception e) {
144+
span.recordException(e);
145+
throw e;
146+
} finally {
147+
span.end();
148+
}
149+
}
150+
151+
/**
152+
* Injects mocked or no-op telemetry and pool instances for unit testing.
153+
*
154+
* @param pool The mocked or test JedisPool instance.
155+
* @param testOpenTelemetry The OpenTelemetry instance to use for testing.
156+
*/
157+
static void initForTest(
158+
final JedisPool pool,
159+
final OpenTelemetry testOpenTelemetry) {
160+
jedisPool = pool;
161+
tracer = testOpenTelemetry.getTracer("jedis.client");
162+
final Meter meter = testOpenTelemetry.getMeter("jedis.metrics");
163+
164+
rttHist = meter.histogramBuilder("redis_client_rtt")
165+
.setUnit("ms").build();
166+
clientBlockHist = meter
167+
.histogramBuilder("redis_client_blocking_latency")
168+
.setUnit("ms").build();
169+
appBlockHist = meter
170+
.histogramBuilder("redis_application_blocking_latency")
171+
.setUnit("ms").build();
172+
retryCounter = meter.counterBuilder("redis_retry_count").build();
173+
connErrorCounter = meter
174+
.counterBuilder("redis_connectivity_error_count")
175+
.build();
176+
177+
retryCounter.add(0, Attributes.of(ATTR_OPERATION, "startup"));
178+
connErrorCounter.add(0, Attributes.of(ATTR_OPERATION, "startup"));
179+
}
180+
181+
/**
182+
* Configures the production OpenTelemetry SDK to export Traces and Metrics
183+
* to Google Cloud Operations.
184+
*/
185+
private static void setupTelemetry() {
186+
final SpanExporter traceExporter =
187+
TraceExporter.createWithDefaultConfiguration();
188+
final SdkTracerProvider tracerProvider =
189+
SdkTracerProvider.builder()
190+
.addSpanProcessor(
191+
BatchSpanProcessor.builder(traceExporter)
192+
.build())
193+
.build();
194+
195+
final MetricExporter metricExporter =
196+
GoogleCloudMetricExporter.createWithDefaultConfiguration();
197+
final SdkMeterProvider meterProvider =
198+
SdkMeterProvider.builder()
199+
.registerMetricReader(
200+
PeriodicMetricReader.builder(metricExporter)
201+
.setInterval(Duration.ofSeconds(
202+
METRIC_INTERVAL_SECONDS))
203+
.build())
204+
.build();
205+
206+
final OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
207+
.setTracerProvider(tracerProvider)
208+
.setMeterProvider(meterProvider)
209+
.buildAndRegisterGlobal();
210+
211+
tracer = openTelemetry.getTracer("jedis.client");
212+
final Meter meter = openTelemetry.getMeter("jedis.metrics");
213+
214+
rttHist = meter.histogramBuilder("redis_client_rtt")
215+
.setUnit("ms").build();
216+
clientBlockHist = meter
217+
.histogramBuilder("redis_client_blocking_latency")
218+
.setUnit("ms").build();
219+
appBlockHist = meter
220+
.histogramBuilder("redis_application_blocking_latency")
221+
.setUnit("ms").build();
222+
retryCounter = meter.counterBuilder("redis_retry_count").build();
223+
connErrorCounter = meter
224+
.counterBuilder("redis_connectivity_error_count")
225+
.build();
226+
227+
retryCounter.add(0, Attributes.of(ATTR_OPERATION, "startup"));
228+
connErrorCounter.add(0, Attributes.of(ATTR_OPERATION, "startup"));
229+
}
230+
231+
/**
232+
* Wraps a Redis operation with latency metrics, reconnection retry logic,
233+
* and trace spans.
234+
*
235+
* @param <T> The return type of the Redis operation.
236+
* @param operationName The name of the operation for metric attributes.
237+
* @param operation The Redis command lambda to execute safely.
238+
* @return The return value from the Redis command.
239+
*/
240+
private static <T> T smartRedisCall(
241+
final String operationName,
242+
final Function<Jedis, T> operation) {
243+
int attempt = 0;
244+
final Attributes attrs = Attributes.of(ATTR_OPERATION,
245+
operationName);
246+
247+
final Span span = tracer.spanBuilder(operationName).startSpan();
248+
249+
try {
250+
while (attempt < MAX_RETRIES) {
251+
final long poolStart = System.nanoTime();
252+
try (Jedis jedis = jedisPool.getResource()) {
253+
clientBlockHist.record((System.nanoTime() - poolStart)
254+
/ NANO_TO_MS, attrs);
255+
256+
final long reqStart = System.nanoTime();
257+
final T response = operation.apply(jedis);
258+
rttHist.record((System.nanoTime() - reqStart)
259+
/ NANO_TO_MS, attrs);
260+
261+
final long appStart = System.nanoTime();
262+
@SuppressWarnings("unused")
263+
final String dummy = String.valueOf(response);
264+
appBlockHist.record((System.nanoTime() - appStart)
265+
/ NANO_TO_MS, attrs);
266+
267+
return response;
268+
} catch (JedisConnectionException e) {
269+
attempt++;
270+
connErrorCounter.add(1, attrs);
271+
retryCounter.add(1, attrs);
272+
span.recordException(e);
273+
if (attempt >= MAX_RETRIES) {
274+
throw e;
275+
}
276+
try {
277+
Thread.sleep((long) (Math.pow(2, attempt)
278+
* RETRY_BACKOFF_BASE_MS));
279+
} catch (InterruptedException ie) {
280+
Thread.currentThread().interrupt();
281+
}
282+
}
283+
}
284+
return null;
285+
} finally {
286+
span.end();
287+
}
288+
}
289+
}
290+
// [END memorystore_redis_client_side_metrics]

0 commit comments

Comments
 (0)