Skip to content

Commit e72ad32

Browse files
authored
Create Client side metrics (#14197)
* Create Client side metrics * Update license * Update main.py * Update main.py * fix:add tests * style: fix style violations * fix:fix comments from reviewer * style:remove unused variable lint * style:remove global imports lint * fix:change version requirements * fix:use pytest version 9.0.3
1 parent f798098 commit e72ad32

4 files changed

Lines changed: 426 additions & 0 deletions

File tree

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START memorystore_redis_client_side_metrics]
16+
import os
17+
import time
18+
19+
from opentelemetry import metrics, trace
20+
from opentelemetry.exporter.cloud_monitoring import (
21+
CloudMonitoringMetricsExporter,
22+
)
23+
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
24+
from opentelemetry.instrumentation.redis import RedisInstrumentor
25+
from opentelemetry.sdk.metrics import MeterProvider
26+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
27+
from opentelemetry.sdk.trace import TracerProvider
28+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
29+
import redis
30+
from redis.exceptions import ConnectionError, TimeoutError
31+
32+
33+
34+
35+
def init_telemetry():
36+
"""Initializes OpenTelemetry with GCP Exporters and returns the SDK objects."""
37+
# 1. Initialize Tracing
38+
tracer_provider = TracerProvider()
39+
tracer_provider.add_span_processor(
40+
BatchSpanProcessor(CloudTraceSpanExporter())
41+
)
42+
trace.set_tracer_provider(tracer_provider)
43+
tracer = trace.get_tracer("redis.client")
44+
45+
# 2. Initialize Metrics
46+
metrics_exporter = CloudMonitoringMetricsExporter()
47+
metric_reader = PeriodicExportingMetricReader(
48+
metrics_exporter, export_interval_millis=10000
49+
)
50+
meter_provider = MeterProvider(metric_readers=[metric_reader])
51+
metrics.set_meter_provider(meter_provider)
52+
meter = metrics.get_meter("redis.metrics")
53+
54+
# Bundle all metric handlers safely into a dictionary
55+
redis_metrics = {
56+
"rtt_hist": meter.create_histogram("redis_client_rtt", unit="ms"),
57+
"client_block_hist": meter.create_histogram(
58+
"redis_client_blocking_latency", unit="ms"
59+
),
60+
"app_block_hist": meter.create_histogram(
61+
"redis_application_blocking_latency", unit="ms"
62+
),
63+
"retry_counter": meter.create_counter("redis_retry_count"),
64+
"conn_error_counter": meter.create_counter(
65+
"redis_connectivity_error_count"
66+
),
67+
}
68+
69+
redis_metrics["retry_counter"].add(0, {"operation": "startup"})
70+
redis_metrics["conn_error_counter"].add(0, {"operation": "startup"})
71+
72+
# 3. Setup Redis Auto-Instrumentation
73+
RedisInstrumentor().instrument()
74+
75+
return tracer, redis_metrics, tracer_provider, meter_provider
76+
77+
78+
def init_redis_pool():
79+
"""Initializes and returns the Redis ConnectionPool and Client."""
80+
redis_host = os.environ.get("REDISHOST", "localhost")
81+
redis_port = int(os.environ.get("REDISPORT", 6379))
82+
83+
redis_pool = redis.ConnectionPool(
84+
host=redis_host,
85+
port=redis_port,
86+
max_connections=10,
87+
decode_responses=True,
88+
)
89+
redis_client = redis.Redis(connection_pool=redis_pool)
90+
return redis_pool, redis_client
91+
92+
93+
def smart_redis_call(
94+
operation_name, func, redis_pool, metrics, *args, **kwargs
95+
):
96+
"""Executes a Redis operation with metrics and retry handling (No Globals!)."""
97+
max_retries = 3
98+
attempt = 0
99+
100+
pool_start = time.time()
101+
try:
102+
conn = redis_pool.get_connection()
103+
redis_pool.release(conn)
104+
except Exception:
105+
pass
106+
107+
if metrics and metrics.get("client_block_hist"):
108+
metrics["client_block_hist"].record(
109+
(time.time() - pool_start) * 1000, {"operation": operation_name}
110+
)
111+
112+
while attempt < max_retries:
113+
try:
114+
req_start = time.time()
115+
response = func(*args, **kwargs)
116+
117+
if metrics and metrics.get("rtt_hist"):
118+
metrics["rtt_hist"].record(
119+
(time.time() - req_start) * 1000,
120+
{"operation": operation_name},
121+
)
122+
123+
app_start = time.time()
124+
_ = str(response)
125+
126+
if metrics and metrics.get("app_block_hist"):
127+
metrics["app_block_hist"].record(
128+
(time.time() - app_start) * 1000,
129+
{"operation": operation_name},
130+
)
131+
132+
return response
133+
134+
except (ConnectionError, TimeoutError) as e:
135+
attempt += 1
136+
if metrics and metrics.get("conn_error_counter"):
137+
metrics["conn_error_counter"].add(
138+
1, {"operation": operation_name}
139+
)
140+
if metrics and metrics.get("retry_counter"):
141+
metrics["retry_counter"].add(1, {"operation": operation_name})
142+
if attempt >= max_retries:
143+
raise e
144+
time.sleep((2**attempt) * 0.1)
145+
146+
if __name__ == "__main__":
147+
tracer, redis_metrics, tracer_provider, meter_provider = init_telemetry()
148+
redis_pool, redis_client = init_redis_pool()
149+
150+
if tracer:
151+
with tracer.start_as_current_span("process_user_span"):
152+
try:
153+
# Simple write and read operations
154+
smart_redis_call(
155+
"set_user",
156+
redis_client.set,
157+
redis_pool,
158+
redis_metrics,
159+
"user:123",
160+
"active",
161+
)
162+
163+
result = smart_redis_call(
164+
"get_user",
165+
redis_client.get,
166+
redis_pool,
167+
redis_metrics,
168+
"user:123",
169+
)
170+
print(f"Retrieved: {result}")
171+
except Exception as e:
172+
print(f"Error: {e}")
173+
174+
tracer_provider.force_flush()
175+
meter_provider.force_flush()
176+
# [END memorystore_redis_client_side_metrics]

0 commit comments

Comments
 (0)