sentry-arroyo 2.24.0__tar.gz → 2.26.0__tar.gz

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.
Files changed (92) hide show
  1. {sentry_arroyo-2.24.0/sentry_arroyo.egg-info → sentry_arroyo-2.26.0}/PKG-INFO +1 -1
  2. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/kafka/__init__.py +6 -1
  3. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/kafka/configuration.py +71 -0
  4. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/processor.py +59 -47
  5. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/metricDefs.json +1 -1
  6. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/metric_defs.py +18 -0
  7. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/metrics.py +48 -1
  8. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0/sentry_arroyo.egg-info}/PKG-INFO +1 -1
  9. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/sentry_arroyo.egg-info/SOURCES.txt +1 -0
  10. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/setup.py +1 -1
  11. sentry_arroyo-2.26.0/tests/backends/test_kafka_producer.py +125 -0
  12. sentry_arroyo-2.26.0/tests/utils/test_metrics.py +84 -0
  13. sentry_arroyo-2.24.0/tests/utils/test_metrics.py +0 -33
  14. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/LICENSE +0 -0
  15. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/MANIFEST.in +0 -0
  16. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/README.md +0 -0
  17. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/__init__.py +0 -0
  18. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/__init__.py +0 -0
  19. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/abstract.py +0 -0
  20. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/kafka/commit.py +0 -0
  21. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/kafka/consumer.py +0 -0
  22. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/local/__init__.py +0 -0
  23. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/local/backend.py +0 -0
  24. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/local/storages/__init__.py +0 -0
  25. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/local/storages/abstract.py +0 -0
  26. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/backends/local/storages/memory.py +0 -0
  27. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/commit.py +0 -0
  28. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/dlq.py +0 -0
  29. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/errors.py +0 -0
  30. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/__init__.py +0 -0
  31. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/__init__.py +0 -0
  32. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/abstract.py +0 -0
  33. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/batching.py +0 -0
  34. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/buffer.py +0 -0
  35. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/commit.py +0 -0
  36. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/filter.py +0 -0
  37. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/guard.py +0 -0
  38. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/healthcheck.py +0 -0
  39. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/noop.py +0 -0
  40. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/produce.py +0 -0
  41. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/reduce.py +0 -0
  42. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/run_task.py +0 -0
  43. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/run_task_in_threads.py +0 -0
  44. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/run_task_with_multiprocessing.py +0 -0
  45. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/processing/strategies/unfold.py +0 -0
  46. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/py.typed +0 -0
  47. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/types.py +0 -0
  48. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/__init__.py +0 -0
  49. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/clock.py +0 -0
  50. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/codecs.py +0 -0
  51. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/concurrent.py +0 -0
  52. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/logging.py +0 -0
  53. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/profiler.py +0 -0
  54. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/arroyo/utils/retries.py +0 -0
  55. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/examples/transform_and_produce/__init__.py +0 -0
  56. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/examples/transform_and_produce/batched.py +0 -0
  57. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/examples/transform_and_produce/script.py +0 -0
  58. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/examples/transform_and_produce/simple.py +0 -0
  59. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/requirements.txt +0 -0
  60. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/sentry_arroyo.egg-info/dependency_links.txt +0 -0
  61. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/sentry_arroyo.egg-info/not-zip-safe +0 -0
  62. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/sentry_arroyo.egg-info/requires.txt +0 -0
  63. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/sentry_arroyo.egg-info/top_level.txt +0 -0
  64. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/setup.cfg +0 -0
  65. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/backends/__init__.py +0 -0
  66. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/backends/mixins.py +0 -0
  67. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/backends/test_commit.py +0 -0
  68. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/backends/test_kafka.py +0 -0
  69. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/backends/test_local.py +0 -0
  70. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/__init__.py +0 -0
  71. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/__init__.py +0 -0
  72. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_all.py +0 -0
  73. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_batching.py +0 -0
  74. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_buffer.py +0 -0
  75. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_commit.py +0 -0
  76. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_filter.py +0 -0
  77. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_guard.py +0 -0
  78. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_noop.py +0 -0
  79. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_produce.py +0 -0
  80. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_reduce.py +0 -0
  81. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_run_task.py +0 -0
  82. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_run_task_in_threads.py +0 -0
  83. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_run_task_with_multiprocessing.py +0 -0
  84. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/strategies/test_unfold.py +0 -0
  85. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/processing/test_processor.py +0 -0
  86. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/test_commit.py +0 -0
  87. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/test_dlq.py +0 -0
  88. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/test_kip848_e2e.py +0 -0
  89. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/test_types.py +0 -0
  90. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/utils/__init__.py +0 -0
  91. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/utils/test_concurrent.py +0 -0
  92. {sentry_arroyo-2.24.0 → sentry_arroyo-2.26.0}/tests/utils/test_retries.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sentry-arroyo
3
- Version: 2.24.0
3
+ Version: 2.26.0
4
4
  Summary: Arroyo is a Python library for working with streaming data.
5
5
  Home-page: https://github.com/getsentry/arroyo
6
6
  Author: Sentry
@@ -1,9 +1,14 @@
1
- from .configuration import build_kafka_configuration, build_kafka_consumer_configuration
1
+ from .configuration import (
2
+ build_kafka_configuration,
3
+ build_kafka_consumer_configuration,
4
+ build_kafka_producer_configuration,
5
+ )
2
6
  from .consumer import KafkaConsumer, KafkaPayload, KafkaProducer
3
7
 
4
8
  __all__ = [
5
9
  "build_kafka_configuration",
6
10
  "build_kafka_consumer_configuration",
11
+ "build_kafka_producer_configuration",
7
12
  "KafkaConsumer",
8
13
  "KafkaPayload",
9
14
  "KafkaProducer",
@@ -49,6 +49,77 @@ def stats_callback(stats_json: str) -> None:
49
49
  )
50
50
 
51
51
 
52
+ def producer_stats_callback(stats_json: str) -> None:
53
+ stats = json.loads(stats_json)
54
+ metrics = get_metrics()
55
+
56
+ # Extract broker-level int_latency metrics
57
+ brokers = stats.get("brokers", {})
58
+ for broker_id, broker_stats in brokers.items():
59
+ int_latency = broker_stats.get("int_latency", {})
60
+ if int_latency:
61
+ p99_latency_ms = int_latency.get("p99", 0) / 1000.0
62
+ metrics.timing(
63
+ "arroyo.producer.librdkafka.p99_int_latency",
64
+ p99_latency_ms,
65
+ tags={"broker_id": str(broker_id)},
66
+ )
67
+ avg_latency_ms = int_latency.get("avg", 0) / 1000.0
68
+ metrics.timing(
69
+ "arroyo.producer.librdkafka.avg_int_latency",
70
+ avg_latency_ms,
71
+ tags={"broker_id": str(broker_id)},
72
+ )
73
+
74
+ outbuf_latency = broker_stats.get("outbuf_latency", {})
75
+ if outbuf_latency:
76
+ p99_latency_ms = outbuf_latency.get("p99", 0) / 1000.0
77
+ metrics.timing(
78
+ "arroyo.producer.librdkafka.p99_outbuf_latency",
79
+ p99_latency_ms,
80
+ tags={"broker_id": str(broker_id)},
81
+ )
82
+ avg_latency_ms = outbuf_latency.get("avg", 0) / 1000.0
83
+ metrics.timing(
84
+ "arroyo.producer.librdkafka.avg_outbuf_latency",
85
+ avg_latency_ms,
86
+ tags={"broker_id": str(broker_id)},
87
+ )
88
+
89
+ rtt = broker_stats.get("rtt", {})
90
+ if rtt:
91
+ p99_rtt_ms = rtt.get("p99", 0) / 1000.0
92
+ metrics.timing(
93
+ "arroyo.producer.librdkafka.p99_rtt",
94
+ p99_rtt_ms,
95
+ tags={"broker_id": str(broker_id)},
96
+ )
97
+ avg_rtt_ms = rtt.get("avg", 0) / 1000.0
98
+ metrics.timing(
99
+ "arroyo.producer.librdkafka.avg_rtt",
100
+ avg_rtt_ms,
101
+ tags={"broker_id": str(broker_id)},
102
+ )
103
+
104
+
105
+ def build_kafka_producer_configuration(
106
+ default_config: Mapping[str, Any],
107
+ bootstrap_servers: Optional[Sequence[str]] = None,
108
+ override_params: Optional[Mapping[str, Any]] = None,
109
+ ) -> KafkaBrokerConfig:
110
+ broker_config = build_kafka_configuration(
111
+ default_config, bootstrap_servers, override_params
112
+ )
113
+
114
+ broker_config.update(
115
+ {
116
+ "statistics.interval.ms": STATS_COLLECTION_FREQ_MS,
117
+ "stats_cb": producer_stats_callback,
118
+ }
119
+ )
120
+ return broker_config
121
+
122
+
52
123
  def build_kafka_consumer_configuration(
53
124
  default_config: Mapping[str, Any],
54
125
  group_id: str,
@@ -29,7 +29,7 @@ from arroyo.processing.strategies.abstract import (
29
29
  )
30
30
  from arroyo.types import BrokerValue, Message, Partition, Topic, TStrategyPayload
31
31
  from arroyo.utils.logging import handle_internal_error
32
- from arroyo.utils.metrics import get_metrics
32
+ from arroyo.utils.metrics import get_consumer_metrics
33
33
 
34
34
  logger = logging.getLogger(__name__)
35
35
 
@@ -90,7 +90,7 @@ ConsumerCounter = Literal[
90
90
 
91
91
  class MetricsBuffer:
92
92
  def __init__(self) -> None:
93
- self.metrics = get_metrics()
93
+ self.metrics = get_consumer_metrics()
94
94
  self.__timers: MutableMapping[ConsumerTiming, float] = defaultdict(float)
95
95
  self.__counters: MutableMapping[ConsumerCounter, int] = defaultdict(int)
96
96
  self.__reset()
@@ -139,6 +139,7 @@ class StreamProcessor(Generic[TStrategyPayload]):
139
139
  commit_policy: CommitPolicy = ONCE_PER_SECOND,
140
140
  dlq_policy: Optional[DlqPolicy[TStrategyPayload]] = None,
141
141
  join_timeout: Optional[float] = None,
142
+ shutdown_strategy_before_consumer: bool = False,
142
143
  ) -> None:
143
144
  self.__consumer = consumer
144
145
  self.__processor_factory = processor_factory
@@ -158,6 +159,7 @@ class StreamProcessor(Generic[TStrategyPayload]):
158
159
  self.__commit_policy_state = commit_policy.get_state_machine()
159
160
  self.__join_timeout = join_timeout
160
161
  self.__shutdown_requested = False
162
+ self.__shutdown_strategy_before_consumer = shutdown_strategy_before_consumer
161
163
 
162
164
  # Buffers messages for DLQ. Messages are added when they are submitted for processing and
163
165
  # removed once the commit callback is fired as they are guaranteed to be valid at that point.
@@ -170,49 +172,7 @@ class StreamProcessor(Generic[TStrategyPayload]):
170
172
  )
171
173
 
172
174
  def _close_strategy() -> None:
173
- start_close = time.time()
174
-
175
- if self.__processing_strategy is None:
176
- # Partitions are revoked when the consumer is shutting down, at
177
- # which point we already have closed the consumer.
178
- return
179
-
180
- logger.info("Closing %r...", self.__processing_strategy)
181
- self.__processing_strategy.close()
182
-
183
- logger.info("Waiting for %r to exit...", self.__processing_strategy)
184
-
185
- while True:
186
- start_join = time.time()
187
-
188
- try:
189
- self.__processing_strategy.join(self.__join_timeout)
190
- self.__metrics_buffer.incr_timing(
191
- "arroyo.consumer.join.time", time.time() - start_join
192
- )
193
- break
194
- except InvalidMessage as e:
195
- self.__metrics_buffer.incr_timing(
196
- "arroyo.consumer.join.time", time.time() - start_join
197
- )
198
- self._handle_invalid_message(e)
199
-
200
- logger.info(
201
- "%r exited successfully, releasing assignment.",
202
- self.__processing_strategy,
203
- )
204
- self.__processing_strategy = None
205
- self.__message = None # avoid leaking buffered messages across assignments
206
- self.__is_paused = False
207
- self._clear_backpressure()
208
-
209
- value = time.time() - start_close
210
-
211
- self.__metrics_buffer.metrics.timing(
212
- "arroyo.consumer.run.close_strategy", value
213
- )
214
-
215
- self.__metrics_buffer.incr_timing("arroyo.consumer.shutdown.time", value)
175
+ self._close_processing_strategy()
216
176
 
217
177
  def _create_strategy(partitions: Mapping[Partition, int]) -> None:
218
178
  start_create = time.time()
@@ -235,8 +195,10 @@ class StreamProcessor(Generic[TStrategyPayload]):
235
195
  def on_partitions_assigned(partitions: Mapping[Partition, int]) -> None:
236
196
  logger.info("New partitions assigned: %r", partitions)
237
197
  logger.info("Member id: %r", self.__consumer.member_id)
198
+ self.__metrics_buffer.metrics.consumer_member_id = self.__consumer.member_id
199
+
238
200
  self.__metrics_buffer.metrics.increment(
239
- "arroyo.consumer.partitions_assigned.count", len(partitions), tags={"consumer_member_id": self.__consumer.member_id}
201
+ "arroyo.consumer.partitions_assigned.count", len(partitions)
240
202
  )
241
203
 
242
204
  current_partitions = dict(self.__consumer.tell())
@@ -244,6 +206,7 @@ class StreamProcessor(Generic[TStrategyPayload]):
244
206
 
245
207
  if self.__dlq_policy:
246
208
  self.__dlq_policy.reset_dlq_limits(current_partitions)
209
+
247
210
  if current_partitions:
248
211
  if self.__processing_strategy is not None:
249
212
  # TODO: for cooperative-sticky rebalancing this can happen
@@ -262,7 +225,7 @@ class StreamProcessor(Generic[TStrategyPayload]):
262
225
  logger.info("Partitions to revoke: %r", partitions)
263
226
 
264
227
  self.__metrics_buffer.metrics.increment(
265
- "arroyo.consumer.partitions_revoked.count", len(partitions), tags={"consumer_member_id": self.__consumer.member_id}
228
+ "arroyo.consumer.partitions_revoked.count", len(partitions)
266
229
  )
267
230
 
268
231
  if partitions:
@@ -299,6 +262,48 @@ class StreamProcessor(Generic[TStrategyPayload]):
299
262
  [topic], on_assign=on_partitions_assigned, on_revoke=on_partitions_revoked
300
263
  )
301
264
 
265
+ def _close_processing_strategy(self) -> None:
266
+ """Close the processing strategy and wait for it to exit."""
267
+ start_close = time.time()
268
+
269
+ if self.__processing_strategy is None:
270
+ # Partitions are revoked when the consumer is shutting down, at
271
+ # which point we already have closed the consumer.
272
+ return
273
+
274
+ logger.info("Closing %r...", self.__processing_strategy)
275
+ logger.info("Member id: %r", self.__consumer.member_id)
276
+ self.__processing_strategy.close()
277
+
278
+ logger.info("Waiting for %r to exit...", self.__processing_strategy)
279
+
280
+ while True:
281
+ start_join = time.time()
282
+
283
+ try:
284
+ self.__processing_strategy.join(self.__join_timeout)
285
+ self.__metrics_buffer.incr_timing(
286
+ "arroyo.consumer.join.time", time.time() - start_join
287
+ )
288
+ break
289
+ except InvalidMessage as e:
290
+ self.__metrics_buffer.incr_timing(
291
+ "arroyo.consumer.join.time", time.time() - start_join
292
+ )
293
+ self._handle_invalid_message(e)
294
+
295
+ logger.info("%r exited successfully", self.__processing_strategy)
296
+ self.__processing_strategy = None
297
+ self.__message = None
298
+ self.__is_paused = False
299
+ self._clear_backpressure()
300
+
301
+ value = time.time() - start_close
302
+ self.__metrics_buffer.metrics.timing(
303
+ "arroyo.consumer.run.close_strategy", value
304
+ )
305
+ self.__metrics_buffer.incr_timing("arroyo.consumer.shutdown.time", value)
306
+
302
307
  def __commit(self, offsets: Mapping[Partition, int], force: bool = False) -> None:
303
308
  """
304
309
  If force is passed, commit immediately and do not throttle. This should
@@ -515,6 +520,13 @@ class StreamProcessor(Generic[TStrategyPayload]):
515
520
  self.__shutdown_requested = True
516
521
 
517
522
  def _shutdown(self) -> None:
523
+ # If shutdown_strategy_before_consumer is set, work around an issue
524
+ # where rdkafka would revoke our partition, but then also immediately
525
+ # revoke our member ID as well, causing join() of the CommitStrategy
526
+ # (that is running in the partition revocation callback) to crash.
527
+ if self.__shutdown_strategy_before_consumer:
528
+ self._close_processing_strategy()
529
+
518
530
  # close the consumer
519
531
  logger.info("Stopping consumer")
520
532
  self.__metrics_buffer.flush()
@@ -1 +1 @@
1
- {"arroyo.strategies.run_task_with_multiprocessing.batch.size.msg": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.size.msg", "type": "Time", "description": "Number of messages in a multiprocessing batch"}, "arroyo.strategies.run_task_with_multiprocessing.batch.size.bytes": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.size.bytes", "type": "Time", "description": "Number of bytes in a multiprocessing batch"}, "arroyo.strategies.run_task_with_multiprocessing.output_batch.size.msg": {"name": "arroyo.strategies.run_task_with_multiprocessing.output_batch.size.msg", "type": "Time", "description": "Number of messages in a multiprocessing batch after the message transformation"}, "arroyo.strategies.run_task_with_multiprocessing.output_batch.size.bytes": {"name": "arroyo.strategies.run_task_with_multiprocessing.output_batch.size.bytes", "type": "Time", "description": "Number of bytes in a multiprocessing batch after the message transformation"}, "arroyo.consumer.run.count": {"name": "arroyo.consumer.run.count", "type": "Counter", "description": "Number of times the consumer is spinning"}, "arroyo.consumer.invalid_message.count": {"name": "arroyo.consumer.invalid_message.count", "type": "Counter", "description": "Number of times the consumer encountered an invalid message."}, "arroyo.strategies.reduce.batch_time": {"name": "arroyo.strategies.reduce.batch_time", "type": "Time", "description": "How long it took the Reduce step to fill up a batch"}, "arroyo.strategies.run_task_with_multiprocessing.batch.backpressure": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.backpressure", "type": "Counter", "description": "Incremented when a strategy after multiprocessing applies\nbackpressure to multiprocessing. May be a reason why CPU cannot be\nsaturated."}, "arroyo.strategies.run_task_with_multiprocessing.batch.input.overflow": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.input.overflow", "type": "Counter", "description": "Incremented when multiprocessing cannot fill the input batch\nbecause not enough memory was allocated. This results in batches smaller\nthan configured. Increase `input_block_size` to fix."}, "arroyo.strategies.run_task_with_multiprocessing.batch.output.overflow": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.output.overflow", "type": "Counter", "description": "Incremented when multiprocessing cannot pull results in batches\nequal to the input batch size, because not enough memory was allocated.\nThis can be devastating for throughput. Increase `output_block_size` to\nfix."}, "arroyo.strategies.run_task_with_multiprocessing.batch.input.resize": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.input.resize", "type": "Counter", "description": "Arroyo has decided to re-allocate a block in order to combat input\nbuffer overflow. This behavior can be disabled by explicitly setting\n`input_block_size` to a not-None value in `RunTaskWithMultiprocessing`."}, "arroyo.strategies.run_task_with_multiprocessing.batch.output.resize": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.output.resize", "type": "Counter", "description": "Arroyo has decided to re-allocate a block in order to combat output\nbuffer overflow. This behavior can be disabled by explicitly setting\n`output_block_size` to a not-None value in `RunTaskWithMultiprocessing`."}, "arroyo.strategies.run_task_with_multiprocessing.batches_in_progress": {"name": "arroyo.strategies.run_task_with_multiprocessing.batches_in_progress", "type": "Gauge", "description": "How many batches are being processed in parallel by multiprocessing."}, "arroyo.strategies.run_task_with_multiprocessing.processes": {"name": "arroyo.strategies.run_task_with_multiprocessing.processes", "type": "Counter", "description": "A subprocess by multiprocessing unexpectedly died.\n\"sigchld.detected\",\nGauge: Shows how many processes the multiprocessing strategy is\nconfigured with."}, "arroyo.strategies.run_task_with_multiprocessing.pool.create": {"name": "arroyo.strategies.run_task_with_multiprocessing.pool.create", "type": "Counter", "description": "Incremented when the multiprocessing pool is created (or re-created)."}, "arroyo.consumer.poll.time": {"name": "arroyo.consumer.poll.time", "type": "Time", "description": "(unitless) spent polling librdkafka for new messages."}, "arroyo.consumer.processing.time": {"name": "arroyo.consumer.processing.time", "type": "Time", "description": "(unitless) spent in strategies (blocking in strategy.submit or\nstrategy.poll)"}, "arroyo.consumer.backpressure.time": {"name": "arroyo.consumer.backpressure.time", "type": "Time", "description": "(unitless) spent pausing the consumer due to backpressure (MessageRejected)"}, "arroyo.consumer.dlq.time": {"name": "arroyo.consumer.dlq.time", "type": "Time", "description": "(unitless) spent in handling `InvalidMessage` exceptions and sending\nmessages to the the DLQ."}, "arroyo.consumer.join.time": {"name": "arroyo.consumer.join.time", "type": "Time", "description": "(unitless) spent in waiting for the strategy to exit, such as during\nshutdown or rebalancing."}, "arroyo.consumer.callback.time": {"name": "arroyo.consumer.callback.time", "type": "Time", "description": "(unitless) spent in librdkafka callbacks. This metric's timings\noverlap other timings, and might spike at the same time."}, "arroyo.consumer.shutdown.time": {"name": "arroyo.consumer.shutdown.time", "type": "Time", "description": "(unitless) spent in shutting down the consumer. This metric's\ntimings overlap other timings, and might spike at the same time."}, "arroyo.consumer.run.callback": {"name": "arroyo.consumer.run.callback", "type": "Time", "description": "A regular duration metric where each datapoint is measuring the time it\ntook to execute a single callback. This metric is distinct from the\narroyo.consumer.*.time metrics as it does not attempt to accumulate time\nspent per second in an attempt to keep monitoring overhead low.\nThe metric is tagged by the name of the internal callback function being\nexecuted, as 'callback_name'. Possible values are on_partitions_assigned\nand on_partitions_revoked."}, "arroyo.consumer.run.close_strategy": {"name": "arroyo.consumer.run.close_strategy", "type": "Time", "description": "Duration metric measuring the time it took to flush in-flight messages\nand shut down the strategies."}, "arroyo.consumer.run.create_strategy": {"name": "arroyo.consumer.run.create_strategy", "type": "Time", "description": "Duration metric measuring the time it took to create the processing strategy."}, "arroyo.consumer.partitions_revoked.count": {"name": "arroyo.consumer.partitions_revoked.count", "type": "Counter", "description": "How many partitions have been revoked just now."}, "arroyo.consumer.partitions_assigned.count": {"name": "arroyo.consumer.partitions_assigned.count", "type": "Counter", "description": "How many partitions have been assigned just now."}, "arroyo.consumer.latency": {"name": "arroyo.consumer.latency", "type": "Time", "description": "Consumer latency in seconds. Recorded by the commit offsets strategy."}, "arroyo.consumer.pause": {"name": "arroyo.consumer.pause", "type": "Counter", "description": "Metric for when the underlying rdkafka consumer is being paused.\nThis flushes internal prefetch buffers."}, "arroyo.consumer.resume": {"name": "arroyo.consumer.resume", "type": "Counter", "description": "Metric for when the underlying rdkafka consumer is being resumed.\nThis might cause increased network usage as messages are being re-fetched."}, "arroyo.consumer.librdkafka.total_queue_size": {"name": "arroyo.consumer.librdkafka.total_queue_size", "type": "Gauge", "description": "Queue size of background queue that librdkafka uses to prefetch messages."}, "arroyo.processing.strategies.healthcheck.touch": {"name": "arroyo.processing.strategies.healthcheck.touch", "type": "Counter", "description": "Counter metric to measure how often the healthcheck file has been touched."}, "arroyo.strategies.filter.dropped_messages": {"name": "arroyo.strategies.filter.dropped_messages", "type": "Counter", "description": "Number of messages dropped in the FilterStep strategy"}, "arroyo.consumer.dlq.dropped_messages": {"name": "arroyo.consumer.dlq.dropped_messages", "type": "Counter", "description": "how many messages are dropped due to errors producing to the dlq"}, "arroyo.consumer.dlq_buffer.len": {"name": "arroyo.consumer.dlq_buffer.len", "type": "Gauge", "description": "Current length of the DLQ buffer deque"}, "arroyo.consumer.dlq_buffer.exceeded": {"name": "arroyo.consumer.dlq_buffer.exceeded", "type": "Counter", "description": "Number of times the DLQ buffer size has been exceeded, causing messages to be dropped"}, "arroyo.consumer.dlq_buffer.assigned_partitions": {"name": "arroyo.consumer.dlq_buffer.assigned_partitions", "type": "Gauge", "description": "Number of partitions being tracked in the DLQ buffer"}}
1
+ {"arroyo.strategies.run_task_with_multiprocessing.batch.size.msg": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.size.msg", "type": "Time", "description": "Number of messages in a multiprocessing batch"}, "arroyo.strategies.run_task_with_multiprocessing.batch.size.bytes": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.size.bytes", "type": "Time", "description": "Number of bytes in a multiprocessing batch"}, "arroyo.strategies.run_task_with_multiprocessing.output_batch.size.msg": {"name": "arroyo.strategies.run_task_with_multiprocessing.output_batch.size.msg", "type": "Time", "description": "Number of messages in a multiprocessing batch after the message transformation"}, "arroyo.strategies.run_task_with_multiprocessing.output_batch.size.bytes": {"name": "arroyo.strategies.run_task_with_multiprocessing.output_batch.size.bytes", "type": "Time", "description": "Number of bytes in a multiprocessing batch after the message transformation"}, "arroyo.consumer.run.count": {"name": "arroyo.consumer.run.count", "type": "Counter", "description": "Number of times the consumer is spinning"}, "arroyo.consumer.invalid_message.count": {"name": "arroyo.consumer.invalid_message.count", "type": "Counter", "description": "Number of times the consumer encountered an invalid message."}, "arroyo.strategies.reduce.batch_time": {"name": "arroyo.strategies.reduce.batch_time", "type": "Time", "description": "How long it took the Reduce step to fill up a batch"}, "arroyo.strategies.run_task_with_multiprocessing.batch.backpressure": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.backpressure", "type": "Counter", "description": "Incremented when a strategy after multiprocessing applies\nbackpressure to multiprocessing. May be a reason why CPU cannot be\nsaturated."}, "arroyo.strategies.run_task_with_multiprocessing.batch.input.overflow": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.input.overflow", "type": "Counter", "description": "Incremented when multiprocessing cannot fill the input batch\nbecause not enough memory was allocated. This results in batches smaller\nthan configured. Increase `input_block_size` to fix."}, "arroyo.strategies.run_task_with_multiprocessing.batch.output.overflow": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.output.overflow", "type": "Counter", "description": "Incremented when multiprocessing cannot pull results in batches\nequal to the input batch size, because not enough memory was allocated.\nThis can be devastating for throughput. Increase `output_block_size` to\nfix."}, "arroyo.strategies.run_task_with_multiprocessing.batch.input.resize": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.input.resize", "type": "Counter", "description": "Arroyo has decided to re-allocate a block in order to combat input\nbuffer overflow. This behavior can be disabled by explicitly setting\n`input_block_size` to a not-None value in `RunTaskWithMultiprocessing`."}, "arroyo.strategies.run_task_with_multiprocessing.batch.output.resize": {"name": "arroyo.strategies.run_task_with_multiprocessing.batch.output.resize", "type": "Counter", "description": "Arroyo has decided to re-allocate a block in order to combat output\nbuffer overflow. This behavior can be disabled by explicitly setting\n`output_block_size` to a not-None value in `RunTaskWithMultiprocessing`."}, "arroyo.strategies.run_task_with_multiprocessing.batches_in_progress": {"name": "arroyo.strategies.run_task_with_multiprocessing.batches_in_progress", "type": "Gauge", "description": "How many batches are being processed in parallel by multiprocessing."}, "arroyo.strategies.run_task_with_multiprocessing.processes": {"name": "arroyo.strategies.run_task_with_multiprocessing.processes", "type": "Counter", "description": "A subprocess by multiprocessing unexpectedly died.\n\"sigchld.detected\",\nGauge: Shows how many processes the multiprocessing strategy is\nconfigured with."}, "arroyo.strategies.run_task_with_multiprocessing.pool.create": {"name": "arroyo.strategies.run_task_with_multiprocessing.pool.create", "type": "Counter", "description": "Incremented when the multiprocessing pool is created (or re-created)."}, "arroyo.consumer.poll.time": {"name": "arroyo.consumer.poll.time", "type": "Time", "description": "(unitless) spent polling librdkafka for new messages."}, "arroyo.consumer.processing.time": {"name": "arroyo.consumer.processing.time", "type": "Time", "description": "(unitless) spent in strategies (blocking in strategy.submit or\nstrategy.poll)"}, "arroyo.consumer.backpressure.time": {"name": "arroyo.consumer.backpressure.time", "type": "Time", "description": "(unitless) spent pausing the consumer due to backpressure (MessageRejected)"}, "arroyo.consumer.dlq.time": {"name": "arroyo.consumer.dlq.time", "type": "Time", "description": "(unitless) spent in handling `InvalidMessage` exceptions and sending\nmessages to the the DLQ."}, "arroyo.consumer.join.time": {"name": "arroyo.consumer.join.time", "type": "Time", "description": "(unitless) spent in waiting for the strategy to exit, such as during\nshutdown or rebalancing."}, "arroyo.consumer.callback.time": {"name": "arroyo.consumer.callback.time", "type": "Time", "description": "(unitless) spent in librdkafka callbacks. This metric's timings\noverlap other timings, and might spike at the same time."}, "arroyo.consumer.shutdown.time": {"name": "arroyo.consumer.shutdown.time", "type": "Time", "description": "(unitless) spent in shutting down the consumer. This metric's\ntimings overlap other timings, and might spike at the same time."}, "arroyo.consumer.run.callback": {"name": "arroyo.consumer.run.callback", "type": "Time", "description": "A regular duration metric where each datapoint is measuring the time it\ntook to execute a single callback. This metric is distinct from the\narroyo.consumer.*.time metrics as it does not attempt to accumulate time\nspent per second in an attempt to keep monitoring overhead low.\nThe metric is tagged by the name of the internal callback function being\nexecuted, as 'callback_name'. Possible values are on_partitions_assigned\nand on_partitions_revoked."}, "arroyo.consumer.run.close_strategy": {"name": "arroyo.consumer.run.close_strategy", "type": "Time", "description": "Duration metric measuring the time it took to flush in-flight messages\nand shut down the strategies."}, "arroyo.consumer.run.create_strategy": {"name": "arroyo.consumer.run.create_strategy", "type": "Time", "description": "Duration metric measuring the time it took to create the processing strategy."}, "arroyo.consumer.partitions_revoked.count": {"name": "arroyo.consumer.partitions_revoked.count", "type": "Counter", "description": "How many partitions have been revoked just now."}, "arroyo.consumer.partitions_assigned.count": {"name": "arroyo.consumer.partitions_assigned.count", "type": "Counter", "description": "How many partitions have been assigned just now."}, "arroyo.consumer.latency": {"name": "arroyo.consumer.latency", "type": "Time", "description": "Consumer latency in seconds. Recorded by the commit offsets strategy."}, "arroyo.consumer.pause": {"name": "arroyo.consumer.pause", "type": "Counter", "description": "Metric for when the underlying rdkafka consumer is being paused.\nThis flushes internal prefetch buffers."}, "arroyo.consumer.resume": {"name": "arroyo.consumer.resume", "type": "Counter", "description": "Metric for when the underlying rdkafka consumer is being resumed.\nThis might cause increased network usage as messages are being re-fetched."}, "arroyo.consumer.librdkafka.total_queue_size": {"name": "arroyo.consumer.librdkafka.total_queue_size", "type": "Gauge", "description": "Queue size of background queue that librdkafka uses to prefetch messages."}, "arroyo.processing.strategies.healthcheck.touch": {"name": "arroyo.processing.strategies.healthcheck.touch", "type": "Counter", "description": "Counter metric to measure how often the healthcheck file has been touched."}, "arroyo.strategies.filter.dropped_messages": {"name": "arroyo.strategies.filter.dropped_messages", "type": "Counter", "description": "Number of messages dropped in the FilterStep strategy"}, "arroyo.consumer.dlq.dropped_messages": {"name": "arroyo.consumer.dlq.dropped_messages", "type": "Counter", "description": "how many messages are dropped due to errors producing to the dlq"}, "arroyo.consumer.dlq_buffer.len": {"name": "arroyo.consumer.dlq_buffer.len", "type": "Gauge", "description": "Current length of the DLQ buffer deque"}, "arroyo.consumer.dlq_buffer.exceeded": {"name": "arroyo.consumer.dlq_buffer.exceeded", "type": "Counter", "description": "Number of times the DLQ buffer size has been exceeded, causing messages to be dropped"}, "arroyo.consumer.dlq_buffer.assigned_partitions": {"name": "arroyo.consumer.dlq_buffer.assigned_partitions", "type": "Gauge", "description": "Number of partitions being tracked in the DLQ buffer"}, "arroyo.producer.librdkafka.p99_int_latency": {"name": "arroyo.producer.librdkafka.p99_int_latency", "type": "Time", "description": "Internal producer queue latency from librdkafka statistics.\nTagged by broker_id."}, "arroyo.producer.librdkafka.p99_outbuf_latency": {"name": "arroyo.producer.librdkafka.p99_outbuf_latency", "type": "Time", "description": "Output buffer latency from librdkafka statistics.\nTagged by broker_id."}, "arroyo.producer.librdkafka.p99_rtt": {"name": "arroyo.producer.librdkafka.p99_rtt", "type": "Time", "description": "Round-trip time to brokers from librdkafka statistics.\nTagged by broker_id."}, "arroyo.producer.librdkafka.avg_int_latency": {"name": "arroyo.producer.librdkafka.avg_int_latency", "type": "Time", "description": "Average internal producer queue latency from librdkafka statistics.\nTagged by broker_id."}, "arroyo.producer.librdkafka.avg_outbuf_latency": {"name": "arroyo.producer.librdkafka.avg_outbuf_latency", "type": "Time", "description": "Average output buffer latency from librdkafka statistics.\nTagged by broker_id."}, "arroyo.producer.librdkafka.avg_rtt": {"name": "arroyo.producer.librdkafka.avg_rtt", "type": "Time", "description": "Average round-trip time to brokers from librdkafka statistics.\nTagged by broker_id."}}
@@ -106,4 +106,22 @@ MetricName = Literal[
106
106
  "arroyo.consumer.dlq_buffer.exceeded",
107
107
  # Gauge: Number of partitions being tracked in the DLQ buffer
108
108
  "arroyo.consumer.dlq_buffer.assigned_partitions",
109
+ # Time: Internal producer queue latency from librdkafka statistics.
110
+ # Tagged by broker_id.
111
+ "arroyo.producer.librdkafka.p99_int_latency",
112
+ # Time: Output buffer latency from librdkafka statistics.
113
+ # Tagged by broker_id.
114
+ "arroyo.producer.librdkafka.p99_outbuf_latency",
115
+ # Time: Round-trip time to brokers from librdkafka statistics.
116
+ # Tagged by broker_id.
117
+ "arroyo.producer.librdkafka.p99_rtt",
118
+ # Time: Average internal producer queue latency from librdkafka statistics.
119
+ # Tagged by broker_id.
120
+ "arroyo.producer.librdkafka.avg_int_latency",
121
+ # Time: Average output buffer latency from librdkafka statistics.
122
+ # Tagged by broker_id.
123
+ "arroyo.producer.librdkafka.avg_outbuf_latency",
124
+ # Time: Average round-trip time to brokers from librdkafka statistics.
125
+ # Tagged by broker_id.
126
+ "arroyo.producer.librdkafka.avg_rtt",
109
127
  ]
@@ -45,6 +45,45 @@ class Metrics(Protocol):
45
45
  raise NotImplementedError
46
46
 
47
47
 
48
+ class ConsumerMetricsWrapper(Metrics):
49
+ """
50
+ A wrapper around a metrics backend that automatically adds consumer_member_id
51
+ to all metrics calls.
52
+
53
+ Right now we only use this to add tags to the metrics emitted by
54
+ StreamProcessor, but ideally all metrics, even those emitted by strategies
55
+ and application code, would get this tag. The metrics abstraction in arroyo
56
+ is not sufficient for this. We'd have to add a "add_global_tags" method
57
+ (similar to the concept of global tags in sentry) and users would have to
58
+ implement it.
59
+ """
60
+
61
+ def __init__(self, metrics: Metrics) -> None:
62
+ self.__metrics = metrics
63
+ self.consumer_member_id = ""
64
+
65
+ def _add_consumer_tag(self, tags: Optional[Tags]) -> Tags:
66
+ return {**(tags or {}), "consumer_member_id": self.consumer_member_id}
67
+
68
+ def increment(
69
+ self,
70
+ name: MetricName,
71
+ value: Union[int, float] = 1,
72
+ tags: Optional[Tags] = None,
73
+ ) -> None:
74
+ self.__metrics.increment(name, value, tags=self._add_consumer_tag(tags))
75
+
76
+ def gauge(
77
+ self, name: MetricName, value: Union[int, float], tags: Optional[Tags] = None
78
+ ) -> None:
79
+ self.__metrics.gauge(name, value, tags=self._add_consumer_tag(tags))
80
+
81
+ def timing(
82
+ self, name: MetricName, value: Union[int, float], tags: Optional[Tags] = None
83
+ ) -> None:
84
+ self.__metrics.timing(name, value, tags=self._add_consumer_tag(tags))
85
+
86
+
48
87
  class DummyMetricsBackend(Metrics):
49
88
  """
50
89
  Default metrics backend that does not record anything.
@@ -133,4 +172,12 @@ def get_metrics() -> Metrics:
133
172
  return _metrics_backend
134
173
 
135
174
 
136
- __all__ = ["configure_metrics", "Metrics", "MetricName"]
175
+ def get_consumer_metrics() -> ConsumerMetricsWrapper:
176
+ """
177
+ Get a metrics backend that automatically adds consumer_member_id to all metrics.
178
+ """
179
+ base_metrics = get_metrics()
180
+ return ConsumerMetricsWrapper(base_metrics)
181
+
182
+
183
+ __all__ = ["configure_metrics", "Metrics", "MetricName", "Tags", "get_consumer_metrics"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sentry-arroyo
3
- Version: 2.24.0
3
+ Version: 2.26.0
4
4
  Summary: Arroyo is a Python library for working with streaming data.
5
5
  Home-page: https://github.com/getsentry/arroyo
6
6
  Author: Sentry
@@ -65,6 +65,7 @@ tests/backends/__init__.py
65
65
  tests/backends/mixins.py
66
66
  tests/backends/test_commit.py
67
67
  tests/backends/test_kafka.py
68
+ tests/backends/test_kafka_producer.py
68
69
  tests/backends/test_local.py
69
70
  tests/processing/__init__.py
70
71
  tests/processing/test_processor.py
@@ -10,7 +10,7 @@ def get_requirements() -> Sequence[str]:
10
10
 
11
11
  setup(
12
12
  name="sentry-arroyo",
13
- version="2.24.0",
13
+ version="2.26.0",
14
14
  author="Sentry",
15
15
  author_email="oss@sentry.io",
16
16
  license="Apache-2.0",
@@ -0,0 +1,125 @@
1
+ import json
2
+ from unittest import mock
3
+
4
+ from arroyo.backends.kafka.configuration import producer_stats_callback
5
+
6
+
7
+ @mock.patch("arroyo.backends.kafka.configuration.get_metrics")
8
+ def test_producer_stats_callback_with_both_latencies(
9
+ mock_get_metrics: mock.Mock,
10
+ ) -> None:
11
+ mock_metrics = mock.Mock()
12
+ mock_get_metrics.return_value = mock_metrics
13
+
14
+ stats_json = json.dumps(
15
+ {
16
+ "brokers": {
17
+ "1": {
18
+ "int_latency": {"p99": 2000, "avg": 1000},
19
+ "outbuf_latency": {"p99": 4000, "avg": 2000},
20
+ }
21
+ }
22
+ }
23
+ )
24
+
25
+ producer_stats_callback(stats_json)
26
+
27
+ assert mock_metrics.timing.call_count == 4
28
+ mock_metrics.timing.assert_any_call(
29
+ "arroyo.producer.librdkafka.p99_int_latency",
30
+ 2.0,
31
+ tags={"broker_id": "1"},
32
+ )
33
+ mock_metrics.timing.assert_any_call(
34
+ "arroyo.producer.librdkafka.avg_int_latency",
35
+ 1.0,
36
+ tags={"broker_id": "1"},
37
+ )
38
+ mock_metrics.timing.assert_any_call(
39
+ "arroyo.producer.librdkafka.p99_outbuf_latency",
40
+ 4.0,
41
+ tags={"broker_id": "1"},
42
+ )
43
+ mock_metrics.timing.assert_any_call(
44
+ "arroyo.producer.librdkafka.avg_outbuf_latency",
45
+ 2.0,
46
+ tags={"broker_id": "1"},
47
+ )
48
+
49
+
50
+ @mock.patch("arroyo.backends.kafka.configuration.get_metrics")
51
+ def test_producer_stats_callback_no_brokers(mock_get_metrics: mock.Mock) -> None:
52
+ mock_metrics = mock.Mock()
53
+ mock_get_metrics.return_value = mock_metrics
54
+
55
+ stats_json = json.dumps({})
56
+
57
+ producer_stats_callback(stats_json)
58
+
59
+ mock_metrics.timing.assert_not_called()
60
+
61
+
62
+ @mock.patch("arroyo.backends.kafka.configuration.get_metrics")
63
+ def test_producer_stats_callback_empty_broker_stats(
64
+ mock_get_metrics: mock.Mock,
65
+ ) -> None:
66
+ mock_metrics = mock.Mock()
67
+ mock_get_metrics.return_value = mock_metrics
68
+
69
+ stats_json = json.dumps({"brokers": {"1": {}}})
70
+
71
+ producer_stats_callback(stats_json)
72
+
73
+ mock_metrics.timing.assert_not_called()
74
+
75
+
76
+ @mock.patch("arroyo.backends.kafka.configuration.get_metrics")
77
+ def test_producer_stats_callback_with_all_metrics(mock_get_metrics: mock.Mock) -> None:
78
+ mock_metrics = mock.Mock()
79
+ mock_get_metrics.return_value = mock_metrics
80
+
81
+ stats_json = json.dumps(
82
+ {
83
+ "brokers": {
84
+ "1": {
85
+ "int_latency": {"p99": 2000, "avg": 1000}, # 2/1 milliseconds
86
+ "outbuf_latency": {"p99": 4000, "avg": 2000}, # 4/2 milliseconds
87
+ "rtt": {"p99": 1500, "avg": 750}, # 1.5/0.75 milliseconds
88
+ }
89
+ }
90
+ }
91
+ )
92
+
93
+ producer_stats_callback(stats_json)
94
+
95
+ assert mock_metrics.timing.call_count == 6
96
+ mock_metrics.timing.assert_any_call(
97
+ "arroyo.producer.librdkafka.p99_int_latency",
98
+ 2.0,
99
+ tags={"broker_id": "1"},
100
+ )
101
+ mock_metrics.timing.assert_any_call(
102
+ "arroyo.producer.librdkafka.avg_int_latency",
103
+ 1.0,
104
+ tags={"broker_id": "1"},
105
+ )
106
+ mock_metrics.timing.assert_any_call(
107
+ "arroyo.producer.librdkafka.p99_outbuf_latency",
108
+ 4.0,
109
+ tags={"broker_id": "1"},
110
+ )
111
+ mock_metrics.timing.assert_any_call(
112
+ "arroyo.producer.librdkafka.avg_outbuf_latency",
113
+ 2.0,
114
+ tags={"broker_id": "1"},
115
+ )
116
+ mock_metrics.timing.assert_any_call(
117
+ "arroyo.producer.librdkafka.p99_rtt",
118
+ 1.5,
119
+ tags={"broker_id": "1"},
120
+ )
121
+ mock_metrics.timing.assert_any_call(
122
+ "arroyo.producer.librdkafka.avg_rtt",
123
+ 0.75,
124
+ tags={"broker_id": "1"},
125
+ )
@@ -0,0 +1,84 @@
1
+ import pytest
2
+
3
+ from arroyo.utils.metrics import (
4
+ Gauge,
5
+ MetricName,
6
+ configure_metrics,
7
+ get_consumer_metrics,
8
+ get_metrics,
9
+ )
10
+ from tests.metrics import Gauge as GaugeCall
11
+ from tests.metrics import (
12
+ Increment,
13
+ TestingMetricsBackend,
14
+ Timing,
15
+ _TestingMetricsBackend,
16
+ )
17
+
18
+
19
+ def test_gauge_simple() -> None:
20
+ backend = TestingMetricsBackend
21
+
22
+ name: MetricName = "name" # type: ignore
23
+ tags = {"tag": "value"}
24
+ gauge = Gauge(backend, name, tags)
25
+
26
+ with gauge:
27
+ pass
28
+
29
+ assert backend.calls == [
30
+ GaugeCall(name, 0.0, tags),
31
+ GaugeCall(name, 1.0, tags),
32
+ GaugeCall(name, 0.0, tags),
33
+ ]
34
+
35
+
36
+ def test_configure_metrics() -> None:
37
+ assert get_metrics() == TestingMetricsBackend
38
+
39
+ with pytest.raises(AssertionError):
40
+ configure_metrics(_TestingMetricsBackend())
41
+
42
+ # Can be reset to something else with force
43
+ configure_metrics(_TestingMetricsBackend(), force=True)
44
+ assert get_metrics() != TestingMetricsBackend
45
+
46
+
47
+ def test_consumer_metrics_wrapper() -> None:
48
+ """Test that ConsumerMetricsWrapper automatically adds consumer_member_id to all metrics."""
49
+ # Reset to a fresh backend
50
+ backend = _TestingMetricsBackend()
51
+ configure_metrics(backend, force=True)
52
+
53
+ consumer_member_id = "test-consumer-123"
54
+ consumer_metrics = get_consumer_metrics()
55
+ consumer_metrics.consumer_member_id = consumer_member_id
56
+
57
+ # Test increment
58
+ consumer_metrics.increment("arroyo.consumer.run.count", 5, tags={"extra": "tag"})
59
+
60
+ # Test gauge
61
+ consumer_metrics.gauge("arroyo.consumer.librdkafka.total_queue_size", 10.5)
62
+
63
+ # Test timing
64
+ consumer_metrics.timing("arroyo.consumer.poll.time", 100, tags={"another": "tag"})
65
+
66
+ expected_calls = [
67
+ Increment(
68
+ "arroyo.consumer.run.count",
69
+ 5,
70
+ {"consumer_member_id": consumer_member_id, "extra": "tag"},
71
+ ),
72
+ GaugeCall(
73
+ "arroyo.consumer.librdkafka.total_queue_size",
74
+ 10.5,
75
+ {"consumer_member_id": consumer_member_id},
76
+ ),
77
+ Timing(
78
+ "arroyo.consumer.poll.time",
79
+ 100,
80
+ {"consumer_member_id": consumer_member_id, "another": "tag"},
81
+ ),
82
+ ]
83
+
84
+ assert backend.calls == expected_calls
@@ -1,33 +0,0 @@
1
- import pytest
2
-
3
- from arroyo.utils.metrics import Gauge, MetricName, configure_metrics, get_metrics
4
- from tests.metrics import Gauge as GaugeCall
5
- from tests.metrics import TestingMetricsBackend, _TestingMetricsBackend
6
-
7
-
8
- def test_gauge_simple() -> None:
9
- backend = TestingMetricsBackend
10
-
11
- name: MetricName = "name" # type: ignore
12
- tags = {"tag": "value"}
13
- gauge = Gauge(backend, name, tags)
14
-
15
- with gauge:
16
- pass
17
-
18
- assert backend.calls == [
19
- GaugeCall(name, 0.0, tags),
20
- GaugeCall(name, 1.0, tags),
21
- GaugeCall(name, 0.0, tags),
22
- ]
23
-
24
-
25
- def test_configure_metrics() -> None:
26
- assert get_metrics() == TestingMetricsBackend
27
-
28
- with pytest.raises(AssertionError):
29
- configure_metrics(_TestingMetricsBackend())
30
-
31
- # Can be reset to something else with force
32
- configure_metrics(_TestingMetricsBackend(), force=True)
33
- assert get_metrics() != TestingMetricsBackend
File without changes
File without changes
File without changes