Skip to content

Commit 4b57026

Browse files
committed
respond to comments
Signed-off-by: Tim Li <[email protected]>
1 parent 3c295f6 commit 4b57026

File tree

6 files changed

+163
-409
lines changed

6 files changed

+163
-409
lines changed
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Visibility and metrics collection components for Cadence client."""
22

3-
from .metrics import MetricsHandler, NoOpMetricsHandler, get_default_handler
4-
from .prometheus import PrometheusMetrics, PrometheusConfig
3+
from .metrics import MetricsEmitter, NoOpMetricsEmitter
4+
from .prometheus import PrometheusMetrics, PrometheusConfig, CadenceMetrics
55

66
__all__ = [
7-
"MetricsHandler",
8-
"NoOpMetricsHandler",
9-
"get_default_handler",
7+
"MetricsEmitter",
8+
"NoOpMetricsEmitter",
109
"PrometheusMetrics",
1110
"PrometheusConfig",
11+
"CadenceMetrics",
1212
]

cadence/_internal/visibility/metrics.py

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ class MetricType(Enum):
1313
COUNTER = "counter"
1414
GAUGE = "gauge"
1515
HISTOGRAM = "histogram"
16-
TIMER = "timer"
1716

1817

19-
class MetricsHandler(Protocol):
18+
class MetricsEmitter(Protocol):
2019
"""Protocol for metrics collection backends."""
2120

2221
def counter(
@@ -31,11 +30,6 @@ def gauge(
3130
"""Send a gauge metric."""
3231
...
3332

34-
def timer(
35-
self, key: str, duration: float, tags: Optional[Dict[str, str]] = None
36-
) -> None:
37-
"""Send a timer metric."""
38-
...
3933

4034
def histogram(
4135
self, key: str, value: float, tags: Optional[Dict[str, str]] = None
@@ -44,8 +38,8 @@ def histogram(
4438
...
4539

4640

47-
class NoOpMetricsHandler:
48-
"""No-op metrics handler that discards all metrics."""
41+
class NoOpMetricsEmitter:
42+
"""No-op metrics emitter that discards all metrics."""
4943

5044
def counter(
5145
self, key: str, n: int = 1, tags: Optional[Dict[str, str]] = None
@@ -57,30 +51,10 @@ def gauge(
5751
) -> None:
5852
pass
5953

60-
def timer(
61-
self, key: str, duration: float, tags: Optional[Dict[str, str]] = None
62-
) -> None:
63-
pass
6454

6555
def histogram(
6656
self, key: str, value: float, tags: Optional[Dict[str, str]] = None
6757
) -> None:
6858
pass
6959

7060

71-
# Global default handler
72-
_default_handler: Optional[MetricsHandler] = None
73-
74-
75-
def get_default_handler() -> MetricsHandler:
76-
"""Get the default global metrics handler."""
77-
global _default_handler
78-
if _default_handler is None:
79-
_default_handler = NoOpMetricsHandler()
80-
return _default_handler
81-
82-
83-
def set_default_handler(handler: MetricsHandler) -> None:
84-
"""Set the default global metrics handler."""
85-
global _default_handler
86-
_default_handler = handler

cadence/_internal/visibility/prometheus.py

Lines changed: 6 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import logging
44
from dataclasses import dataclass, field
5-
from typing import Dict, Optional, Any
5+
from enum import Enum
6+
from typing import Dict, Optional
67

78
from prometheus_client import ( # type: ignore[import-not-found]
89
REGISTRY,
@@ -11,10 +12,10 @@
1112
Gauge,
1213
Histogram,
1314
generate_latest,
14-
push_to_gateway,
15-
start_http_server,
1615
)
1716

17+
from cadence._internal.visibility.metrics import MetricsEmitter
18+
1819

1920
logger = logging.getLogger(__name__)
2021

@@ -23,16 +24,6 @@
2324
class PrometheusConfig:
2425
"""Configuration for Prometheus metrics."""
2526

26-
# HTTP server configuration
27-
enable_http_server: bool = False
28-
http_port: int = 8000
29-
http_addr: str = "0.0.0.0"
30-
31-
# Push gateway configuration
32-
enable_push_gateway: bool = False
33-
push_gateway_url: str = "localhost:9091"
34-
push_job_name: str = "cadence_client"
35-
3627
# Metric name prefix
3728
metric_prefix: str = "cadence_"
3829

@@ -43,7 +34,7 @@ class PrometheusConfig:
4334
registry: Optional[CollectorRegistry] = None
4435

4536

46-
class PrometheusMetrics:
37+
class PrometheusMetrics(MetricsEmitter):
4738
"""Prometheus metrics collector implementation."""
4839

4940
def __init__(self, config: Optional[PrometheusConfig] = None):
@@ -55,12 +46,6 @@ def __init__(self, config: Optional[PrometheusConfig] = None):
5546
self._gauges: Dict[str, Gauge] = {}
5647
self._histograms: Dict[str, Histogram] = {}
5748

58-
# HTTP server handle
59-
self._http_server: Optional[Any] = None
60-
61-
if self.config.enable_http_server:
62-
self.start_http_server()
63-
6449
def _get_metric_name(self, name: str) -> str:
6550
"""Get the full metric name with prefix."""
6651
return f"{self.config.metric_prefix}{name}"
@@ -126,23 +111,6 @@ def _get_or_create_histogram(
126111

127112
return self._histograms[metric_name]
128113

129-
def _get_or_create_timer(
130-
self, name: str, labels: Optional[Dict[str, str]]
131-
) -> Histogram:
132-
"""Get or create a timer metric (implemented as histogram)."""
133-
metric_name = self._get_metric_name(name)
134-
135-
if metric_name not in self._histograms:
136-
label_names = list(self._merge_labels(labels).keys()) if labels else []
137-
self._histograms[metric_name] = Histogram(
138-
metric_name,
139-
f"Timer metric for {name}",
140-
labelnames=label_names,
141-
registry=self.registry,
142-
)
143-
logger.debug(f"Created timer metric: {metric_name}")
144-
145-
return self._histograms[metric_name]
146114

147115
def counter(
148116
self, key: str, n: int = 1, tags: Optional[Dict[str, str]] = None
@@ -176,21 +144,6 @@ def gauge(
176144
except Exception as e:
177145
logger.error(f"Failed to send gauge {key}: {e}")
178146

179-
def timer(
180-
self, key: str, duration: float, tags: Optional[Dict[str, str]] = None
181-
) -> None:
182-
"""Send a timer metric - implemented as histogram."""
183-
try:
184-
timer = self._get_or_create_timer(key, tags)
185-
merged_tags = self._merge_labels(tags)
186-
187-
if merged_tags:
188-
timer.labels(**merged_tags).observe(duration)
189-
else:
190-
timer.observe(duration)
191-
192-
except Exception as e:
193-
logger.error(f"Failed to send timer {key}: {e}")
194147

195148
def histogram(
196149
self, key: str, value: float, tags: Optional[Dict[str, str]] = None
@@ -208,44 +161,6 @@ def histogram(
208161
except Exception as e:
209162
logger.error(f"Failed to send histogram {key}: {e}")
210163

211-
def start_http_server(self) -> None:
212-
"""Start HTTP server to expose metrics."""
213-
if self._http_server is not None:
214-
logger.warning("HTTP server already started")
215-
return
216-
217-
try:
218-
server_result = start_http_server(
219-
self.config.http_port,
220-
addr=self.config.http_addr,
221-
registry=self.registry,
222-
)
223-
self._http_server = server_result
224-
logger.info(
225-
f"Prometheus metrics HTTP server started on "
226-
f"{self.config.http_addr}:{self.config.http_port}"
227-
)
228-
except Exception as e:
229-
logger.error(f"Failed to start HTTP server: {e}")
230-
raise
231-
232-
def push_to_gateway(self) -> None:
233-
"""Push metrics to Prometheus Push Gateway."""
234-
if not self.config.enable_push_gateway:
235-
logger.warning("Push gateway not enabled")
236-
return
237-
238-
try:
239-
push_to_gateway(
240-
self.config.push_gateway_url,
241-
job=self.config.push_job_name,
242-
registry=self.registry,
243-
)
244-
logger.debug(f"Pushed metrics to gateway: {self.config.push_gateway_url}")
245-
except Exception as e:
246-
logger.error(f"Failed to push to gateway: {e}")
247-
raise
248-
249164
def get_metrics_text(self) -> str:
250165
"""Get metrics in Prometheus text format."""
251166
try:
@@ -255,19 +170,9 @@ def get_metrics_text(self) -> str:
255170
logger.error(f"Failed to generate metrics text: {e}")
256171
return ""
257172

258-
def shutdown(self) -> None:
259-
"""Shutdown the metrics collector."""
260-
if self._http_server:
261-
try:
262-
self._http_server.shutdown()
263-
self._http_server = None
264-
logger.info("Prometheus HTTP server shutdown")
265-
except Exception as e:
266-
logger.error(f"Failed to shutdown HTTP server: {e}")
267-
268173

269174
# Default Cadence metrics names
270-
class CadenceMetrics:
175+
class CadenceMetrics(Enum):
271176
"""Standard Cadence client metrics."""
272177

273178
# Workflow metrics

cadence/client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from grpc.aio import Channel, ClientInterceptor, secure_channel, insecure_channel
1313
from cadence.api.v1.service_workflow_pb2_grpc import WorkflowAPIStub
1414
from cadence.data_converter import DataConverter, DefaultDataConverter
15+
from cadence._internal.visibility.metrics import MetricsEmitter, NoOpMetricsEmitter
1516

1617

1718
class ClientOptions(TypedDict, total=False):
@@ -24,6 +25,7 @@ class ClientOptions(TypedDict, total=False):
2425
channel_arguments: dict[str, Any]
2526
credentials: ChannelCredentials | None
2627
compression: Compression
28+
metrics_emitter: MetricsEmitter
2729
interceptors: list[ClientInterceptor]
2830

2931
_DEFAULT_OPTIONS: ClientOptions = {
@@ -34,6 +36,7 @@ class ClientOptions(TypedDict, total=False):
3436
"channel_arguments": {},
3537
"credentials": None,
3638
"compression": Compression.NoCompression,
39+
"metrics_emitter": NoOpMetricsEmitter(),
3740
"interceptors": [],
3841
}
3942

@@ -69,6 +72,10 @@ def worker_stub(self) -> WorkerAPIStub:
6972
def workflow_stub(self) -> WorkflowAPIStub:
7073
return self._workflow_stub
7174

75+
@property
76+
def metrics_emitter(self) -> MetricsEmitter:
77+
return self._options["metrics_emitter"]
78+
7279
async def ready(self) -> None:
7380
await self._channel.channel_ready()
7481

tests/cadence/_internal/visibility/test_metrics.py

Lines changed: 19 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,48 @@
44

55

66
from cadence._internal.visibility.metrics import (
7-
MetricsHandler,
7+
MetricsEmitter,
88
MetricType,
9-
NoOpMetricsHandler,
10-
get_default_handler,
11-
set_default_handler,
9+
NoOpMetricsEmitter,
1210
)
1311

1412

15-
class TestMetricsHandler:
16-
"""Test cases for MetricsHandler protocol."""
13+
class TestMetricsEmitter:
14+
"""Test cases for MetricsEmitter protocol."""
1715

18-
def test_noop_handler(self):
19-
"""Test no-op handler doesn't raise exceptions."""
20-
handler = NoOpMetricsHandler()
16+
def test_noop_emitter(self):
17+
"""Test no-op emitter doesn't raise exceptions."""
18+
emitter = NoOpMetricsEmitter()
2119

2220
# Should not raise any exceptions
23-
handler.counter("test_counter", 1)
24-
handler.gauge("test_gauge", 42.0)
25-
handler.histogram("test_histogram", 0.5)
26-
handler.timer("test_timing", 1.5)
21+
emitter.counter("test_counter", 1)
22+
emitter.gauge("test_gauge", 42.0)
23+
emitter.histogram("test_histogram", 0.5)
2724

28-
def test_mock_handler(self):
29-
"""Test mock handler implementation."""
30-
mock_handler = Mock(spec=MetricsHandler)
25+
def test_mock_emitter(self):
26+
"""Test mock emitter implementation."""
27+
mock_emitter = Mock(spec=MetricsEmitter)
3128

3229
# Test counter
33-
mock_handler.counter("test_counter", 2, {"label": "value"})
34-
mock_handler.counter.assert_called_once_with(
30+
mock_emitter.counter("test_counter", 2, {"label": "value"})
31+
mock_emitter.counter.assert_called_once_with(
3532
"test_counter", 2, {"label": "value"}
3633
)
3734

3835
# Test gauge
39-
mock_handler.gauge("test_gauge", 100.0, {"env": "test"})
40-
mock_handler.gauge.assert_called_once_with(
36+
mock_emitter.gauge("test_gauge", 100.0, {"env": "test"})
37+
mock_emitter.gauge.assert_called_once_with(
4138
"test_gauge", 100.0, {"env": "test"}
4239
)
4340

44-
# Test timer
45-
mock_handler.timer("test_timing", 0.75, {"tag": "value"})
46-
mock_handler.timer.assert_called_once_with(
47-
"test_timing", 0.75, {"tag": "value"}
48-
)
4941

5042
# Test histogram
51-
mock_handler.histogram("test_histogram", 2.5, {"env": "prod"})
52-
mock_handler.histogram.assert_called_once_with(
43+
mock_emitter.histogram("test_histogram", 2.5, {"env": "prod"})
44+
mock_emitter.histogram.assert_called_once_with(
5345
"test_histogram", 2.5, {"env": "prod"}
5446
)
5547

5648

57-
class TestDefaultHandler:
58-
"""Test cases for default handler management."""
59-
60-
def test_get_default_handler(self):
61-
"""Test getting the default handler."""
62-
handler = get_default_handler()
63-
assert isinstance(handler, NoOpMetricsHandler)
64-
65-
# Should return the same instance
66-
handler2 = get_default_handler()
67-
assert handler is handler2
68-
69-
def test_set_default_handler(self):
70-
"""Test setting a custom default handler."""
71-
original_handler = get_default_handler()
72-
custom_handler = NoOpMetricsHandler()
73-
74-
set_default_handler(custom_handler)
75-
76-
# Should return the custom handler
77-
current_handler = get_default_handler()
78-
assert current_handler is custom_handler
79-
assert current_handler is not original_handler
80-
81-
# Restore original for other tests
82-
set_default_handler(original_handler)
8349

8450

8551
class TestMetricType:
@@ -90,4 +56,3 @@ def test_metric_type_values(self):
9056
assert MetricType.COUNTER.value == "counter"
9157
assert MetricType.GAUGE.value == "gauge"
9258
assert MetricType.HISTOGRAM.value == "histogram"
93-
assert MetricType.TIMER.value == "timer"

0 commit comments

Comments
 (0)