Skip to content
This repository was archived by the owner on Mar 24, 2025. It is now read-only.

Commit f6da834

Browse files
authored
Add default prometheus metrics to clients (#652)
1 parent 297cc85 commit f6da834

File tree

8 files changed

+167
-10
lines changed

8 files changed

+167
-10
lines changed

baseplate/clients/memcache/__init__.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Tuple
99
from typing import Union
1010

11+
from prometheus_client import Gauge
1112
from pymemcache.client.base import PooledClient
1213

1314
from baseplate import Span
@@ -112,7 +113,7 @@ def parse(self, key_path: str, raw_config: config.RawConfig) -> "MemcacheContext
112113
serializer=self.serializer,
113114
deserializer=self.deserializer,
114115
)
115-
return MemcacheContextFactory(pool)
116+
return MemcacheContextFactory(pool, key_path)
116117

117118

118119
class MemcacheContextFactory(ContextFactory):
@@ -129,9 +130,35 @@ class MemcacheContextFactory(ContextFactory):
129130
130131
"""
131132

132-
def __init__(self, pooled_client: PooledClient):
133+
PROM_PREFIX = "bp_memcached_pool"
134+
PROM_LABELS = ["pool"]
135+
136+
pool_size_gauge = Gauge(
137+
f"{PROM_PREFIX}_max_size",
138+
"Maximum number of connections allowed in this pool",
139+
PROM_LABELS,
140+
)
141+
142+
used_connections_gauge = Gauge(
143+
f"{PROM_PREFIX}_active_connections",
144+
"Number of connections in this pool currently in use",
145+
PROM_LABELS,
146+
)
147+
148+
free_connections_gauge = Gauge(
149+
f"{PROM_PREFIX}_free_connections",
150+
"Number of free connections in this pool",
151+
PROM_LABELS,
152+
)
153+
154+
def __init__(self, pooled_client: PooledClient, name: str = "default"):
133155
self.pooled_client = pooled_client
134156

157+
pool = self.pooled_client.client_pool
158+
self.pool_size_gauge.labels(name).set_function(lambda: pool.max_size)
159+
self.free_connections_gauge.labels(name).set_function(lambda: len(pool.free))
160+
self.used_connections_gauge.labels(name).set_function(lambda: len(pool.used))
161+
135162
def report_memcache_runtime_metrics(self, batch: metrics.Client) -> None:
136163
pool = self.pooled_client.client_pool
137164
batch.gauge("pool.in_use").replace(len(pool.used))

baseplate/clients/redis.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import redis
77

8+
from prometheus_client import Gauge
9+
810
# redis.client.StrictPipeline was renamed to redis.client.Pipeline in version 3.0
911
try:
1012
from redis.client import StrictPipeline as Pipeline # type: ignore
@@ -75,7 +77,7 @@ def __init__(self, **kwargs: Any):
7577

7678
def parse(self, key_path: str, raw_config: config.RawConfig) -> "RedisContextFactory":
7779
connection_pool = pool_from_config(raw_config, f"{key_path}.", **self.kwargs)
78-
return RedisContextFactory(connection_pool)
80+
return RedisContextFactory(connection_pool, key_path)
7981

8082

8183
class RedisContextFactory(ContextFactory):
@@ -92,9 +94,35 @@ class RedisContextFactory(ContextFactory):
9294
9395
"""
9496

95-
def __init__(self, connection_pool: redis.ConnectionPool):
97+
PROM_PREFIX = "bp_redis_pool"
98+
PROM_LABELS = ["pool"]
99+
100+
max_connections = Gauge(
101+
f"{PROM_PREFIX}_max_size",
102+
"Maximum number of connections allowed in this redisbp pool",
103+
PROM_LABELS,
104+
)
105+
idle_connections = Gauge(
106+
f"{PROM_PREFIX}_idle_connections",
107+
"Number of idle connections in this redisbp pool",
108+
PROM_LABELS,
109+
)
110+
open_connections = Gauge(
111+
f"{PROM_PREFIX}_active_connections",
112+
"Number of open connections in this redisbp pool",
113+
PROM_LABELS,
114+
)
115+
116+
def __init__(self, connection_pool: redis.ConnectionPool, name: str = "redis"):
96117
self.connection_pool = connection_pool
97118

119+
if isinstance(connection_pool, redis.BlockingConnectionPool):
120+
self.max_connections.labels(name).set_function(lambda: connection_pool.max_connections)
121+
self.idle_connections.labels(name).set_function(connection_pool.pool.qsize)
122+
self.open_connections.labels(name).set_function(
123+
lambda: len(connection_pool._connections) # type: ignore
124+
)
125+
98126
def report_runtime_metrics(self, batch: metrics.Client) -> None:
99127
if not isinstance(self.connection_pool, redis.BlockingConnectionPool):
100128
return

baseplate/clients/redis_cluster.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import rediscluster
1111

12+
from prometheus_client import Gauge
1213
from rediscluster.pipeline import ClusterPipeline
1314

1415
from baseplate import Span
@@ -331,7 +332,7 @@ def __init__(self, **kwargs: Any):
331332

332333
def parse(self, key_path: str, raw_config: config.RawConfig) -> "ClusterRedisContextFactory":
333334
connection_pool = cluster_pool_from_config(raw_config, f"{key_path}.", **self.kwargs)
334-
return ClusterRedisContextFactory(connection_pool)
335+
return ClusterRedisContextFactory(connection_pool, key_path)
335336

336337

337338
class ClusterRedisContextFactory(ContextFactory):
@@ -346,9 +347,33 @@ class ClusterRedisContextFactory(ContextFactory):
346347
:param connection_pool: A connection pool.
347348
"""
348349

349-
def __init__(self, connection_pool: rediscluster.ClusterConnectionPool):
350+
PROM_PREFIX = "bp_redis_cluster_pool"
351+
PROM_LABELS = ["pool"]
352+
353+
max_connections_gauge = Gauge(
354+
f"{PROM_PREFIX}_max_size",
355+
"Maximum number of connections allowed in this redis cluster pool",
356+
PROM_LABELS,
357+
)
358+
open_connections_gauge = Gauge(
359+
f"{PROM_PREFIX}_open_connections",
360+
"Number of open connections in this redis cluster pool",
361+
PROM_LABELS,
362+
)
363+
364+
def __init__(
365+
self, connection_pool: rediscluster.ClusterConnectionPool, name: str = "redis_cluster"
366+
):
350367
self.connection_pool = connection_pool
351368

369+
if isinstance(connection_pool, rediscluster.ClusterBlockingConnectionPool):
370+
self.max_connections_gauge.labels(name).set_function(
371+
lambda: connection_pool.max_connections
372+
)
373+
self.open_connections_gauge.labels(name).set_function(
374+
lambda: len(connection_pool._connections)
375+
)
376+
352377
def report_runtime_metrics(self, batch: metrics.Client) -> None:
353378
if not isinstance(self.connection_pool, rediscluster.ClusterBlockingConnectionPool):
354379
return

baseplate/clients/sqlalchemy.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Tuple
88
from typing import Union
99

10+
from prometheus_client import Gauge
1011
from sqlalchemy import create_engine
1112
from sqlalchemy import event
1213
from sqlalchemy.engine import Connection
@@ -125,7 +126,7 @@ def parse(
125126
engine = engine_from_config(
126127
raw_config, secrets=self.secrets, prefix=f"{key_path}.", **self.kwargs
127128
)
128-
return SQLAlchemySessionContextFactory(engine)
129+
return SQLAlchemySessionContextFactory(engine, key_path)
129130

130131

131132
Parameters = Optional[Union[Dict[str, Any], Sequence[Any]]]
@@ -155,12 +156,46 @@ class SQLAlchemyEngineContextFactory(ContextFactory):
155156
156157
"""
157158

158-
def __init__(self, engine: Engine):
159+
PROM_PREFIX = "bp_sqlalchemy_pool"
160+
PROM_LABELS = ["pool"]
161+
162+
max_connections_gauge = Gauge(
163+
f"{PROM_PREFIX}_max_size",
164+
"Maximum number of connections allowed in this pool",
165+
PROM_LABELS,
166+
)
167+
168+
checked_in_connections_gauge = Gauge(
169+
f"{PROM_PREFIX}_idle_connections",
170+
"Number of available, checked in, connections in this pool",
171+
PROM_LABELS,
172+
)
173+
174+
checked_out_connections_gauge = Gauge(
175+
f"{PROM_PREFIX}_active_connections",
176+
"Number of connections in use, or checked out, in this pool",
177+
PROM_LABELS,
178+
)
179+
180+
overflow_connections_gauge = Gauge(
181+
f"{PROM_PREFIX}_overflow_connections",
182+
"Number of connections over the desired size of this pool",
183+
PROM_LABELS,
184+
)
185+
186+
def __init__(self, engine: Engine, name: str = "sqlalchemy"):
159187
self.engine = engine.execution_options()
160188
event.listen(self.engine, "before_cursor_execute", self.on_before_execute, retval=True)
161189
event.listen(self.engine, "after_cursor_execute", self.on_after_execute)
162190
event.listen(self.engine, "handle_error", self.on_error)
163191

192+
pool = self.engine.pool
193+
if isinstance(pool, QueuePool):
194+
self.max_connections_gauge.labels(name).set_function(pool.size)
195+
self.checked_in_connections_gauge.labels(name).set_function(pool.checkedin)
196+
self.checked_out_connections_gauge.labels(name).set_function(pool.checkedout)
197+
self.overflow_connections_gauge.labels(name).set_function(pool.overflow)
198+
164199
def report_runtime_metrics(self, batch: metrics.Client) -> None:
165200
pool = self.engine.pool
166201
if not isinstance(pool, QueuePool):

baseplate/clients/thrift.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Iterator
99
from typing import Optional
1010

11+
from prometheus_client import Gauge
1112
from thrift.protocol.TProtocol import TProtocolException
1213
from thrift.Thrift import TApplicationException
1314
from thrift.Thrift import TException
@@ -70,6 +71,21 @@ class ThriftContextFactory(ContextFactory):
7071
7172
"""
7273

74+
PROM_PREFIX = "bp_thrift_pool"
75+
PROM_LABELS = ["client_cls"]
76+
77+
max_connections_gauge = Gauge(
78+
f"{PROM_PREFIX}_max_size",
79+
"Maximum number of connections in this thrift pool before blocking",
80+
PROM_LABELS,
81+
)
82+
83+
active_connections_gauge = Gauge(
84+
f"{PROM_PREFIX}_active_connections",
85+
"Number of connections currently in use in this thrift pool",
86+
PROM_LABELS,
87+
)
88+
7389
def __init__(self, pool: ThriftConnectionPool, client_cls: Any):
7490
self.pool = pool
7591
self.client_cls = client_cls
@@ -83,6 +99,10 @@ def __init__(self, pool: ThriftConnectionPool, client_cls: Any):
8399
},
84100
)
85101

102+
pool_name = type(self.client_cls).__name__
103+
self.max_connections_gauge.labels(pool_name).set_function(lambda: self.pool.size)
104+
self.active_connections_gauge.labels(pool_name).set_function(lambda: self.pool.checkedout)
105+
86106
def report_runtime_metrics(self, batch: metrics.Client) -> None:
87107
batch.gauge("pool.size").replace(self.pool.size)
88108
batch.gauge("pool.in_use").replace(self.pool.checkedout)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"requests>=2.21.0,<3.0",
4444
"thrift-unofficial>=0.14.1,<1.0",
4545
"gevent>=20.5.0",
46+
"prometheus-client>=0.12.0",
4647
],
4748
extras_require=extras_require,
4849
scripts=[

tests/unit/clients/memcache_tests.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
del pymemcache
1212

1313
from baseplate.lib.config import ConfigurationError
14-
from baseplate.clients.memcache import pool_from_config
14+
from baseplate.clients.memcache import pool_from_config, MemcacheContextFactory
1515
from baseplate.clients.memcache import lib as memcache_lib
1616

1717

@@ -54,6 +54,17 @@ def test_nodelay(self):
5454
)
5555
self.assertEqual(pool.no_delay, False)
5656

57+
def test_metrics(self):
58+
max_pool_size = "123"
59+
ctx = MemcacheContextFactory(
60+
pool_from_config(
61+
{"memcache.endpoint": "localhost:1234", "memcache.max_pool_size": max_pool_size}
62+
)
63+
)
64+
metric = ctx.pool_size_gauge.collect()[0]
65+
sample = [sample for sample in metric.samples if sample.labels["pool"] == "default"][0]
66+
self.assertEqual(sample.value, float(max_pool_size))
67+
5768

5869
class SerdeTests(unittest.TestCase):
5970
def test_serialize_str(self):

tests/unit/clients/redis_tests.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
del redis
99

1010
from baseplate.lib.config import ConfigurationError
11-
from baseplate.clients.redis import pool_from_config
11+
from baseplate.clients.redis import pool_from_config, RedisContextFactory
1212

1313

1414
class PoolFromConfigTests(unittest.TestCase):
@@ -23,6 +23,16 @@ def test_basic_url(self):
2323
self.assertEqual(pool.connection_kwargs["port"], 1234)
2424
self.assertEqual(pool.connection_kwargs["db"], 0)
2525

26+
def test_metrics(self):
27+
max_connections = "123"
28+
ctx = RedisContextFactory(
29+
pool_from_config(
30+
{"redis.url": "redis://localhost:1234/0", "redis.max_connections": max_connections}
31+
)
32+
)
33+
metric = ctx.max_connections.collect()
34+
self.assertEqual(metric[0].samples[0].value, float(max_connections))
35+
2636
def test_timeouts(self):
2737
pool = pool_from_config(
2838
{

0 commit comments

Comments
 (0)