python-etcd3-async 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- etcd3/__init__.py +90 -0
- etcd3/async_client.py +1495 -0
- etcd3/async_client_extensions.py +878 -0
- etcd3/async_endpoint.py +140 -0
- etcd3/async_managers.py +498 -0
- etcd3/async_monitor.py +16 -0
- etcd3/async_watch.py +462 -0
- etcd3/client.py +2021 -0
- etcd3/endpoint.py +79 -0
- etcd3/events.py +40 -0
- etcd3/exceptions.py +47 -0
- etcd3/exporters/__init__.py +18 -0
- etcd3/exporters/prometheus.py +365 -0
- etcd3/leases.py +106 -0
- etcd3/locks.py +593 -0
- etcd3/log.py +146 -0
- etcd3/log_formatters.py +228 -0
- etcd3/log_sampling.py +221 -0
- etcd3/members.py +44 -0
- etcd3/observability.py +431 -0
- etcd3/shared.py +486 -0
- etcd3/trace.py +231 -0
- etcd3/trace_attributes.py +434 -0
- etcd3/transactions.py +280 -0
- etcd3/utils.py +158 -0
- etcd3/watch.py +270 -0
- etcdrpc/__init__.py +16 -0
- etcdrpc/auth.proto +49 -0
- etcdrpc/auth_pb2.py +43 -0
- etcdrpc/auth_pb2.pyi +44 -0
- etcdrpc/auth_pb2_grpc.py +24 -0
- etcdrpc/kv.proto +43 -0
- etcdrpc/kv_pb2.py +40 -0
- etcdrpc/kv_pb2.pyi +39 -0
- etcdrpc/kv_pb2_grpc.py +24 -0
- etcdrpc/rpc.proto +924 -0
- etcdrpc/rpc_pb2.py +237 -0
- etcdrpc/rpc_pb2.pyi +811 -0
- etcdrpc/rpc_pb2_grpc.py +1893 -0
- python_etcd3_async-0.1.0.dist-info/METADATA +328 -0
- python_etcd3_async-0.1.0.dist-info/RECORD +42 -0
- python_etcd3_async-0.1.0.dist-info/WHEEL +4 -0
etcd3/endpoint.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Base endpoint implementation for etcd3 client.
|
|
2
|
+
|
|
3
|
+
This module provides the BaseEndpoint class that contains shared logic
|
|
4
|
+
for both synchronous and asynchronous endpoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseEndpoint:
|
|
12
|
+
"""Base endpoint with shared logic.
|
|
13
|
+
|
|
14
|
+
:param str host: Endpoint host
|
|
15
|
+
:param int port: Endpoint port
|
|
16
|
+
:param bool secure: Use secure channel, default True
|
|
17
|
+
:param creds: Credentials to use for secure channel, required if
|
|
18
|
+
secure=True
|
|
19
|
+
:type creds: grpc.ChannelCredentials, optional
|
|
20
|
+
:param time_retry: Seconds to wait before retrying this endpoint after
|
|
21
|
+
failure, default 300.0
|
|
22
|
+
:type time_retry: int or float
|
|
23
|
+
:param opts: Additional gRPC options
|
|
24
|
+
:type opts: dict, optional
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, host, port, secure=True, creds=None, time_retry=300.0, opts=None
|
|
29
|
+
):
|
|
30
|
+
self.host = host
|
|
31
|
+
self.netloc = f"{host}:{port}"
|
|
32
|
+
self.secure = secure
|
|
33
|
+
self.protocol = "https" if secure else "http"
|
|
34
|
+
if self.secure and creds is None:
|
|
35
|
+
raise ValueError("Please set TLS credentials for secure connections")
|
|
36
|
+
self.credentials = creds
|
|
37
|
+
self.time_retry = time_retry
|
|
38
|
+
self.opts = opts
|
|
39
|
+
self.in_use = False
|
|
40
|
+
self.last_failed = 0
|
|
41
|
+
self.channel = None # To be set by subclasses
|
|
42
|
+
|
|
43
|
+
def fail(self):
|
|
44
|
+
"""Transition the endpoint to a failed state."""
|
|
45
|
+
self.in_use = False
|
|
46
|
+
self.last_failed = time.time()
|
|
47
|
+
|
|
48
|
+
def use(self):
|
|
49
|
+
"""Transition the endpoint to an active state."""
|
|
50
|
+
if self.is_failed():
|
|
51
|
+
raise ValueError("Trying to use a failed node")
|
|
52
|
+
self.in_use = True
|
|
53
|
+
self.last_failed = 0
|
|
54
|
+
return self.channel
|
|
55
|
+
|
|
56
|
+
def is_failed(self):
|
|
57
|
+
"""Check if the current endpoint is failed."""
|
|
58
|
+
return (time.time() - self.last_failed) < self.time_retry
|
|
59
|
+
|
|
60
|
+
def is_healthy(self):
|
|
61
|
+
"""Check if the endpoint is healthy.
|
|
62
|
+
|
|
63
|
+
Only READY state is considered healthy. CONNECTING state is excluded
|
|
64
|
+
to ensure the connection is fully established before use.
|
|
65
|
+
|
|
66
|
+
:returns: True if the endpoint is healthy (READY state)
|
|
67
|
+
:rtype: bool
|
|
68
|
+
"""
|
|
69
|
+
if self.channel is None:
|
|
70
|
+
return False
|
|
71
|
+
try:
|
|
72
|
+
state = self.channel.get_state()
|
|
73
|
+
# Only READY is considered healthy, not CONNECTING
|
|
74
|
+
return state == grpc.ChannelConnectivity.READY
|
|
75
|
+
except Exception:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def __str__(self):
|
|
79
|
+
return f"Endpoint({self.protocol}://{self.netloc})"
|
etcd3/events.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Event(object):
|
|
2
|
+
def __init__(self, event):
|
|
3
|
+
self.key = event.kv.key
|
|
4
|
+
self._event = event
|
|
5
|
+
|
|
6
|
+
def __getattr__(self, name):
|
|
7
|
+
if name.startswith("prev_"):
|
|
8
|
+
return getattr(self._event.prev_kv, name[5:])
|
|
9
|
+
return getattr(self._event.kv, name)
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
return "{type} key={key} value={value}".format(
|
|
13
|
+
type=self.__class__, key=self.key, value=self.value
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PutEvent(Event):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeleteEvent(Event):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def new_event(event):
|
|
26
|
+
"""
|
|
27
|
+
Wrap a raw gRPC event in a friendlier containing class.
|
|
28
|
+
|
|
29
|
+
This picks the appropriate class from one of PutEvent or DeleteEvent and
|
|
30
|
+
returns a new instance.
|
|
31
|
+
"""
|
|
32
|
+
op_name = event.EventType.DESCRIPTOR.values_by_number[event.type].name
|
|
33
|
+
if op_name == "PUT":
|
|
34
|
+
cls = PutEvent
|
|
35
|
+
elif op_name == "DELETE":
|
|
36
|
+
cls = DeleteEvent
|
|
37
|
+
else:
|
|
38
|
+
raise Exception("Invalid op_name")
|
|
39
|
+
|
|
40
|
+
return cls(event)
|
etcd3/exceptions.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class Etcd3Exception(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WatchTimedOut(Etcd3Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InternalServerError(Etcd3Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConnectionFailedError(Etcd3Exception):
|
|
14
|
+
def __str__(self):
|
|
15
|
+
return "etcd connection failed"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConnectionTimeoutError(Etcd3Exception):
|
|
19
|
+
def __str__(self):
|
|
20
|
+
return "etcd connection timeout"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PreconditionFailedError(Etcd3Exception):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RevisionCompactedError(Etcd3Exception):
|
|
28
|
+
def __init__(self, compacted_revision):
|
|
29
|
+
self.compacted_revision = compacted_revision
|
|
30
|
+
super(RevisionCompactedError, self).__init__()
|
|
31
|
+
|
|
32
|
+
def __str__(self):
|
|
33
|
+
return "etcd revision {} has been compacted".format(self.compacted_revision)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NoServerAvailableError(Etcd3Exception):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LockReleaseError(Etcd3Exception):
|
|
41
|
+
"""Raised when a lock release operation fails."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BatchOperationError(Etcd3Exception):
|
|
46
|
+
"""Raised when a batch operation fails."""
|
|
47
|
+
pass
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Exporters for etcd3 observability.
|
|
2
|
+
|
|
3
|
+
This package provides various exporters for metrics and traces.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from etcd3.exporters.prometheus import (
|
|
7
|
+
EtcdPrometheusExporter,
|
|
8
|
+
get_exporter,
|
|
9
|
+
init_exporter,
|
|
10
|
+
is_prometheus_available,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"EtcdPrometheusExporter",
|
|
15
|
+
"get_exporter",
|
|
16
|
+
"init_exporter",
|
|
17
|
+
"is_prometheus_available",
|
|
18
|
+
]
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Prometheus metrics exporter for etcd3 client.
|
|
2
|
+
|
|
3
|
+
This module provides Prometheus-compatible metrics export for the etcd3 client,
|
|
4
|
+
supporting various metric types including counters, gauges, and histograms.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Optional
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from prometheus_client import CollectorRegistry
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from prometheus_client import (
|
|
17
|
+
Counter,
|
|
18
|
+
Gauge,
|
|
19
|
+
Histogram,
|
|
20
|
+
CollectorRegistry,
|
|
21
|
+
generate_latest,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
PROMETHEUS_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
PROMETHEUS_AVAILABLE = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("etcd3.exporters")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EtcdPrometheusExporter:
|
|
33
|
+
"""Prometheus metrics exporter for etcd3 client.
|
|
34
|
+
|
|
35
|
+
Provides Prometheus-compatible metrics with labels for operation,
|
|
36
|
+
endpoint, and status dimensions.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> exporter = EtcdPrometheusExporter()
|
|
40
|
+
>>> exporter.record_request("kv.get", endpoint="localhost:2379", duration=0.001)
|
|
41
|
+
>>> print(exporter.metrics()) # Get Prometheus format output
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, registry: Optional[CollectorRegistry] = None):
|
|
45
|
+
"""Initialize Prometheus exporter.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
registry: Optional Prometheus registry. Uses default if None.
|
|
49
|
+
"""
|
|
50
|
+
if not PROMETHEUS_AVAILABLE:
|
|
51
|
+
raise ImportError(
|
|
52
|
+
"prometheus_client is required for metrics export. "
|
|
53
|
+
"Install with: pip install prometheus-client"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self._registry = registry or CollectorRegistry()
|
|
57
|
+
|
|
58
|
+
# Request metrics
|
|
59
|
+
self._request_counter = Counter(
|
|
60
|
+
"etcd3_requests_total",
|
|
61
|
+
"Total number of requests",
|
|
62
|
+
["operation", "endpoint", "status"],
|
|
63
|
+
registry=self._registry,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self._request_duration = Histogram(
|
|
67
|
+
"etcd3_request_duration_seconds",
|
|
68
|
+
"Request duration in seconds",
|
|
69
|
+
["operation", "endpoint"],
|
|
70
|
+
buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
|
|
71
|
+
registry=self._registry,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Error metrics
|
|
75
|
+
self._error_counter = Counter(
|
|
76
|
+
"etcd3_errors_total",
|
|
77
|
+
"Total number of errors",
|
|
78
|
+
["operation", "endpoint", "error_type"],
|
|
79
|
+
registry=self._registry,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Retry metrics
|
|
83
|
+
self._retry_counter = Counter(
|
|
84
|
+
"etcd3_retries_total",
|
|
85
|
+
"Total number of retries",
|
|
86
|
+
["operation", "endpoint"],
|
|
87
|
+
registry=self._registry,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Connection metrics
|
|
91
|
+
self._connection_gauge = Gauge(
|
|
92
|
+
"etcd3_connections",
|
|
93
|
+
"Number of active connections",
|
|
94
|
+
["endpoint", "state"],
|
|
95
|
+
registry=self._registry,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def record_request(
|
|
99
|
+
self,
|
|
100
|
+
operation: str,
|
|
101
|
+
endpoint: str,
|
|
102
|
+
duration: float,
|
|
103
|
+
status: str = "success",
|
|
104
|
+
):
|
|
105
|
+
"""Record a request metric.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
operation: Operation name (e.g., "kv.get", "lease.grant")
|
|
109
|
+
endpoint: Endpoint address
|
|
110
|
+
duration: Request duration in seconds
|
|
111
|
+
status: Request status ("success" or "error")
|
|
112
|
+
"""
|
|
113
|
+
status_label = "success" if status in ("success", "ok", 200, 0) else "error"
|
|
114
|
+
|
|
115
|
+
self._request_counter.labels(
|
|
116
|
+
operation=operation,
|
|
117
|
+
endpoint=endpoint,
|
|
118
|
+
status=status_label,
|
|
119
|
+
).inc()
|
|
120
|
+
|
|
121
|
+
self._request_duration.labels(
|
|
122
|
+
operation=operation,
|
|
123
|
+
endpoint=endpoint,
|
|
124
|
+
).observe(duration)
|
|
125
|
+
|
|
126
|
+
def record_error(
|
|
127
|
+
self,
|
|
128
|
+
operation: str,
|
|
129
|
+
endpoint: str,
|
|
130
|
+
error_type: str,
|
|
131
|
+
):
|
|
132
|
+
"""Record an error metric.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
operation: Operation name
|
|
136
|
+
endpoint: Endpoint address
|
|
137
|
+
error_type: Error type/class name
|
|
138
|
+
"""
|
|
139
|
+
self._error_counter.labels(
|
|
140
|
+
operation=operation,
|
|
141
|
+
endpoint=endpoint,
|
|
142
|
+
error_type=error_type,
|
|
143
|
+
).inc()
|
|
144
|
+
|
|
145
|
+
def record_retry(
|
|
146
|
+
self,
|
|
147
|
+
operation: str,
|
|
148
|
+
endpoint: str,
|
|
149
|
+
):
|
|
150
|
+
"""Record a retry metric.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
operation: Operation name
|
|
154
|
+
endpoint: Endpoint address
|
|
155
|
+
"""
|
|
156
|
+
self._retry_counter.labels(
|
|
157
|
+
operation=operation,
|
|
158
|
+
endpoint=endpoint,
|
|
159
|
+
).inc()
|
|
160
|
+
|
|
161
|
+
def set_connection_state(
|
|
162
|
+
self,
|
|
163
|
+
endpoint: str,
|
|
164
|
+
state: str,
|
|
165
|
+
):
|
|
166
|
+
"""Set connection state gauge.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
endpoint: Endpoint address
|
|
170
|
+
state: Connection state ("active", "idle", "failed")
|
|
171
|
+
"""
|
|
172
|
+
self._connection_gauge.labels(
|
|
173
|
+
endpoint=endpoint,
|
|
174
|
+
state=state,
|
|
175
|
+
).set(1)
|
|
176
|
+
|
|
177
|
+
def unset_connection(self, endpoint: str):
|
|
178
|
+
"""Unset connection for an endpoint.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
endpoint: Endpoint address
|
|
182
|
+
"""
|
|
183
|
+
for state in ("active", "idle", "failed"):
|
|
184
|
+
try:
|
|
185
|
+
self._connection_gauge.labels(
|
|
186
|
+
endpoint=endpoint,
|
|
187
|
+
state=state,
|
|
188
|
+
).dec(1)
|
|
189
|
+
except ValueError:
|
|
190
|
+
pass # Value already 0
|
|
191
|
+
|
|
192
|
+
def record_endpoint_failure(self, endpoint: str):
|
|
193
|
+
"""Record an endpoint failure."""
|
|
194
|
+
self.set_connection_state(endpoint, "failed")
|
|
195
|
+
|
|
196
|
+
def record_endpoint_usage(self, endpoint: str):
|
|
197
|
+
"""Record an endpoint usage."""
|
|
198
|
+
self.record_request("endpoint.usage", endpoint, 0, "success")
|
|
199
|
+
|
|
200
|
+
def record_endpoint_recovery(self, endpoint: str):
|
|
201
|
+
"""Record an endpoint recovery."""
|
|
202
|
+
self.set_connection_state(endpoint, "active")
|
|
203
|
+
|
|
204
|
+
def metrics(self) -> bytes:
|
|
205
|
+
"""Get metrics in Prometheus format.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Prometheus metrics text format
|
|
209
|
+
"""
|
|
210
|
+
return generate_latest(self._registry)
|
|
211
|
+
|
|
212
|
+
def reset(self):
|
|
213
|
+
"""Reset all metrics."""
|
|
214
|
+
logger.warning("Reset not fully supported in Prometheus exporter")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Global exporter instance
|
|
218
|
+
_exporter: Optional[EtcdPrometheusExporter] = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_exporter() -> Optional[EtcdPrometheusExporter]:
|
|
222
|
+
"""Get the global Prometheus exporter instance.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Global EtcdPrometheusExporter instance or None if not available
|
|
226
|
+
"""
|
|
227
|
+
global _exporter
|
|
228
|
+
if _exporter is None and PROMETHEUS_AVAILABLE:
|
|
229
|
+
try:
|
|
230
|
+
_exporter = EtcdPrometheusExporter()
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.warning(f"Failed to create Prometheus exporter: {e}")
|
|
233
|
+
return None
|
|
234
|
+
return _exporter
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def init_exporter(
|
|
238
|
+
registry: Optional[CollectorRegistry] = None,
|
|
239
|
+
) -> EtcdPrometheusExporter:
|
|
240
|
+
"""Initialize the global Prometheus exporter.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
registry: Optional Prometheus registry
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Initialized EtcdPrometheusExporter instance
|
|
247
|
+
"""
|
|
248
|
+
global _exporter
|
|
249
|
+
_exporter = EtcdPrometheusExporter(registry)
|
|
250
|
+
return _exporter
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def is_prometheus_available() -> bool:
|
|
254
|
+
"""Check if Prometheus client is available.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if prometheus_client is installed
|
|
258
|
+
"""
|
|
259
|
+
return PROMETHEUS_AVAILABLE
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class _NoOpMetrics:
|
|
263
|
+
"""No-op metrics collector when Prometheus is not available."""
|
|
264
|
+
|
|
265
|
+
def record_request(
|
|
266
|
+
self, operation: str, endpoint: str, duration: float, status: str = "success"
|
|
267
|
+
):
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
def record_error(self, operation: str, endpoint: str, error_type: str):
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
def record_retry(self, operation: str, endpoint: str):
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
def record_endpoint_failure(self, endpoint: str):
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
def record_endpoint_usage(self, endpoint: str):
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
def record_endpoint_recovery(self, endpoint: str):
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
def set_connection_state(self, endpoint: str, state: str):
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
def unset_connection(self, endpoint: str):
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
def metrics(self) -> bytes:
|
|
292
|
+
return b""
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def get_metrics_interface():
|
|
296
|
+
"""Get the metrics interface.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
EtcdPrometheusExporter if available, otherwise NoOpMetrics
|
|
300
|
+
"""
|
|
301
|
+
if PROMETHEUS_AVAILABLE:
|
|
302
|
+
exporter = get_exporter()
|
|
303
|
+
if exporter is not None:
|
|
304
|
+
return exporter
|
|
305
|
+
return _NoOpMetrics()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def monitor_request(operation: str, is_async: bool = False):
|
|
309
|
+
"""Decorator factory to monitor request latency and errors.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
operation: Name of the operation
|
|
313
|
+
is_async: Whether the decorated function is async
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Decorator function
|
|
317
|
+
"""
|
|
318
|
+
import time
|
|
319
|
+
|
|
320
|
+
def decorator(func):
|
|
321
|
+
if is_async:
|
|
322
|
+
|
|
323
|
+
async def async_wrapper(*args, **kwargs):
|
|
324
|
+
start_time = time.time()
|
|
325
|
+
try:
|
|
326
|
+
result = await func(*args, **kwargs)
|
|
327
|
+
latency = time.time() - start_time
|
|
328
|
+
get_metrics_interface().record_request(
|
|
329
|
+
operation, "unknown", latency, "success"
|
|
330
|
+
)
|
|
331
|
+
return result
|
|
332
|
+
except Exception as e:
|
|
333
|
+
latency = time.time() - start_time
|
|
334
|
+
get_metrics_interface().record_request(
|
|
335
|
+
operation, "unknown", latency, "error"
|
|
336
|
+
)
|
|
337
|
+
get_metrics_interface().record_error(
|
|
338
|
+
operation, "unknown", type(e).__name__
|
|
339
|
+
)
|
|
340
|
+
raise
|
|
341
|
+
|
|
342
|
+
return async_wrapper
|
|
343
|
+
|
|
344
|
+
def sync_wrapper(*args, **kwargs):
|
|
345
|
+
start_time = time.time()
|
|
346
|
+
try:
|
|
347
|
+
result = func(*args, **kwargs)
|
|
348
|
+
latency = time.time() - start_time
|
|
349
|
+
get_metrics_interface().record_request(
|
|
350
|
+
operation, "unknown", latency, "success"
|
|
351
|
+
)
|
|
352
|
+
return result
|
|
353
|
+
except Exception as e:
|
|
354
|
+
latency = time.time() - start_time
|
|
355
|
+
get_metrics_interface().record_request(
|
|
356
|
+
operation, "unknown", latency, "error"
|
|
357
|
+
)
|
|
358
|
+
get_metrics_interface().record_error(
|
|
359
|
+
operation, "unknown", type(e).__name__
|
|
360
|
+
)
|
|
361
|
+
raise
|
|
362
|
+
|
|
363
|
+
return sync_wrapper
|
|
364
|
+
|
|
365
|
+
return decorator
|
etcd3/leases.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
class Lease(object):
|
|
2
|
+
"""
|
|
3
|
+
A lease.
|
|
4
|
+
|
|
5
|
+
:ivar id: ID of the lease
|
|
6
|
+
:ivar ttl: time to live for this lease
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, lease_id, ttl, etcd_client=None):
|
|
10
|
+
if ttl <= 0:
|
|
11
|
+
raise ValueError("ttl must be a positive integer")
|
|
12
|
+
self.id = lease_id
|
|
13
|
+
self.ttl = ttl
|
|
14
|
+
|
|
15
|
+
self.etcd_client = etcd_client
|
|
16
|
+
|
|
17
|
+
def _get_lease_info(self):
|
|
18
|
+
return self.etcd_client.get_lease_info(self.id)
|
|
19
|
+
|
|
20
|
+
def revoke(self):
|
|
21
|
+
"""Revoke this lease."""
|
|
22
|
+
self.etcd_client.revoke_lease(self.id)
|
|
23
|
+
|
|
24
|
+
def refresh(self):
|
|
25
|
+
"""Refresh the time to live for this lease.
|
|
26
|
+
|
|
27
|
+
Returns a generator that yields keep-alive responses.
|
|
28
|
+
The caller should iterate over this generator to keep the lease alive.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
for response in lease.refresh():
|
|
32
|
+
# Lease is kept alive while iterating
|
|
33
|
+
pass
|
|
34
|
+
"""
|
|
35
|
+
return self.etcd_client.refresh_lease(self.id)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def remaining_ttl(self):
|
|
39
|
+
return self._get_lease_info().TTL
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def granted_ttl(self):
|
|
43
|
+
return self._get_lease_info().grantedTTL
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def keys(self):
|
|
47
|
+
return self._get_lease_info().keys
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AsyncLease(object):
|
|
51
|
+
"""
|
|
52
|
+
An asynchronous lease.
|
|
53
|
+
|
|
54
|
+
:ivar id: ID of the lease
|
|
55
|
+
:ivar ttl: time to live for this lease
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, lease_id, ttl, etcd_client=None):
|
|
59
|
+
if ttl <= 0:
|
|
60
|
+
raise ValueError("ttl must be a positive integer")
|
|
61
|
+
self.id = lease_id
|
|
62
|
+
self.ttl = ttl
|
|
63
|
+
|
|
64
|
+
self.etcd_client = etcd_client
|
|
65
|
+
|
|
66
|
+
async def _get_lease_info(self):
|
|
67
|
+
"""Get lease information from etcd."""
|
|
68
|
+
return await self.etcd_client.get_lease_info(self.id)
|
|
69
|
+
|
|
70
|
+
async def revoke(self):
|
|
71
|
+
"""Revoke this lease."""
|
|
72
|
+
await self.etcd_client.revoke_lease(self.id)
|
|
73
|
+
|
|
74
|
+
def refresh(self):
|
|
75
|
+
"""Refresh the time to live for this lease.
|
|
76
|
+
|
|
77
|
+
Returns an async generator that yields keep-alive responses.
|
|
78
|
+
"""
|
|
79
|
+
return self.etcd_client.refresh_lease(self.id)
|
|
80
|
+
|
|
81
|
+
async def get_remaining_ttl(self):
|
|
82
|
+
"""Get the remaining time to live for this lease.
|
|
83
|
+
|
|
84
|
+
:returns: remaining TTL in seconds
|
|
85
|
+
:rtype: int
|
|
86
|
+
"""
|
|
87
|
+
info = await self._get_lease_info()
|
|
88
|
+
return info.TTL
|
|
89
|
+
|
|
90
|
+
async def get_granted_ttl(self):
|
|
91
|
+
"""Get the granted time to live for this lease.
|
|
92
|
+
|
|
93
|
+
:returns: granted TTL in seconds
|
|
94
|
+
:rtype: int
|
|
95
|
+
"""
|
|
96
|
+
info = await self._get_lease_info()
|
|
97
|
+
return info.grantedTTL
|
|
98
|
+
|
|
99
|
+
async def get_keys(self):
|
|
100
|
+
"""Get the keys attached to this lease.
|
|
101
|
+
|
|
102
|
+
:returns: list of keys attached to this lease
|
|
103
|
+
:rtype: list
|
|
104
|
+
"""
|
|
105
|
+
info = await self._get_lease_info()
|
|
106
|
+
return list(info.keys)
|