Skip to content

Commit f14f3b8

Browse files
authored
Merge pull request #3 from Lightricks/feature/events
Events: support events.
2 parents 8a6b4c0 + 4c26430 commit f14f3b8

24 files changed

+625
-216
lines changed

pyformance/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from .registry import dump_metrics, clear
66
from .decorators import count_calls, meter_calls, hist_calls, time_calls
77
from .meters.timer import call_too_long
8+
from .mark_int import MarkInt

pyformance/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.4"
1+
__version__ = "1.0.0"

pyformance/mark_int.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class MarkInt:
2+
"""
3+
Mark metric value as and integer.
4+
5+
Reporters such as influx require consistent data types for metrics and require you
6+
to mark integer values with an "i" suffix. This is here to let Influx know it should
7+
do so for the value it's initialized with.
8+
"""
9+
def __init__(self, value):
10+
self.value = int(value)
11+
12+
def __str__(self):
13+
return str(self.value)
14+
15+
def __repr__(self):
16+
return f"MarkInt({self.value})"
17+
18+
def __eq__(self, other):
19+
return isinstance(other, MarkInt) and other.value == self.value

pyformance/meters/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
from .histogram import Histogram
44
from .timer import Timer
55
from .gauge import Gauge, CallbackGauge, SimpleGauge
6-
from .base_metric import BaseMetric
6+
from .base_metric import BaseMetric
7+
from .event import Event, EventPoint

pyformance/meters/event.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import copy
2+
from dataclasses import dataclass
3+
from threading import Lock
4+
from typing import Any, Dict
5+
6+
from .base_metric import BaseMetric
7+
8+
9+
@dataclass
10+
class EventPoint:
11+
time: int
12+
values: Dict[str, Any]
13+
14+
15+
class Event(BaseMetric):
16+
"""
17+
Report events as specific data points in specific timestamps
18+
19+
This meter is outside of DropWizard's models and is here to support a specific use case of
20+
infrequently running cron like operations that trigger once in a while, do a bunch of work
21+
and dump the metric results for a single timestamp. Unlike all the other meter types, this one
22+
doesn't repeat itself if no activity occurs leading you to think everything is running
23+
constantly and producing data when it is not.
24+
25+
The closest you can get to the same effect without this class is by using a Gauge, setting the
26+
value, invoking report_now, than clearing it right after.
27+
Since those operations above are not within a lock shared by scheduled reporters , it can still
28+
report the gauge twice.
29+
30+
Additionally when using gauges you don't have any control over the name of the field writen to
31+
(just metric name and tags), and can't write a bunch of
32+
values at once but resort to writing values to separate Gauges which will make the lack of
33+
lock condition more likely to be an issue.
34+
35+
Another problem that will pop in such usage is that the metric will still be written, it will
36+
just be written with the initial value of 0, so you won't be able to tell when was the last
37+
successful run with ease.
38+
"""
39+
40+
def __init__(self, clock, key, tags=None):
41+
super(Event, self).__init__(key, tags)
42+
self.lock = Lock()
43+
self.points = []
44+
self.clock = clock
45+
46+
def add(self, values: Dict[str, Any]):
47+
with self.lock:
48+
self.points.append(EventPoint(
49+
time=self.clock.time(),
50+
values=values
51+
))
52+
53+
def clear(self):
54+
with self.lock:
55+
self.points = []
56+
57+
def get_events(self):
58+
with self.lock:
59+
return copy.copy(self.points)

pyformance/registry.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import re
22
import time
3-
from .meters import Counter, Histogram, Meter, Timer, Gauge, CallbackGauge, SimpleGauge, BaseMetric
3+
from typing import Dict
44

5+
from .meters import BaseMetric, CallbackGauge, Counter, Event, Gauge, Histogram, Meter, \
6+
SimpleGauge, Timer
57

6-
class MetricsRegistry(object):
78

9+
class MetricsRegistry(object):
810
"""
911
A single interface used to gather metrics on a service. It keeps track of
1012
all the relevant Counters, Meters, Histograms, and Timers. It does not have
@@ -21,6 +23,7 @@ def __init__(self, clock=time):
2123
self._counters = {}
2224
self._histograms = {}
2325
self._gauges = {}
26+
self._events = {}
2427
self._clock = clock
2528

2629
def add(self, key, metric, tags=None):
@@ -42,6 +45,7 @@ def add(self, key, metric, tags=None):
4245
(Gauge, self._gauges),
4346
(Timer, self._timers),
4447
(Counter, self._counters),
48+
(Event, self._events),
4549
)
4650
for cls, registry in class_map:
4751
if isinstance(metric, cls):
@@ -144,11 +148,28 @@ def timer(self, key, tags=None):
144148
)
145149
return self._timers[metric_key]
146150

151+
def event(self, key: str, tags: Dict[str, str] = None) -> Event:
152+
"""
153+
Gets an event reporter based on key and tags
154+
:param key: The metric name / measurement name
155+
:param tags: Tags to attach to the metric
156+
:return: Event object you can add readings to
157+
"""
158+
metric_key = BaseMetric(key, tags)
159+
if metric_key not in self._events:
160+
self._events[metric_key] = Event(
161+
clock=self._clock,
162+
key=key,
163+
tags=tags
164+
)
165+
return self._events[metric_key]
166+
147167
def clear(self):
148168
self._meters.clear()
149169
self._counters.clear()
150170
self._gauges.clear()
151171
self._timers.clear()
172+
self._events.clear()
152173
self._histograms.clear()
153174

154175
def _get_counter_metrics(self, metric_key):
@@ -196,6 +217,19 @@ def _get_meter_metrics(self, metric_key):
196217
return res
197218
return {}
198219

220+
def _get_event_metrics(self, metric_key):
221+
if metric_key in self._events:
222+
_event = self._events[metric_key]
223+
points = _event.get_events()
224+
225+
if points:
226+
res = {
227+
"events": points
228+
}
229+
230+
return res
231+
return {}
232+
199233
def _get_timer_metrics(self, metric_key):
200234
if metric_key in self._timers:
201235
timer = self._timers[metric_key]
@@ -237,11 +271,12 @@ def get_metrics(self, key, tags=None):
237271
def _get_metrics_by_metric_key(self, metric_key):
238272
metrics = {}
239273
for getter in (
240-
self._get_counter_metrics,
241-
self._get_histogram_metrics,
242-
self._get_meter_metrics,
243-
self._get_timer_metrics,
244-
self._get_gauge_metrics,
274+
self._get_counter_metrics,
275+
self._get_histogram_metrics,
276+
self._get_meter_metrics,
277+
self._get_timer_metrics,
278+
self._get_gauge_metrics,
279+
self._get_event_metrics,
245280
):
246281
metrics.update(getter(metric_key))
247282
return metrics
@@ -257,11 +292,12 @@ def dump_metrics(self, key_is_metric=False):
257292
"""
258293
metrics = {}
259294
for metric_type in (
260-
self._counters,
261-
self._histograms,
262-
self._meters,
263-
self._timers,
264-
self._gauges,
295+
self._counters,
296+
self._histograms,
297+
self._meters,
298+
self._timers,
299+
self._gauges,
300+
self._events,
265301
):
266302
for metric_key in metric_type.keys():
267303
if key_is_metric:
@@ -271,12 +307,15 @@ def dump_metrics(self, key_is_metric=False):
271307

272308
metrics[key] = self._get_metrics_by_metric_key(metric_key)
273309

310+
# Don't repeat events, that's the whole point of events
311+
for _event in self._events.values():
312+
_event.clear()
313+
274314
return metrics
275315

276316

277317
# TODO make sure tags are supported properly
278318
class RegexRegistry(MetricsRegistry):
279-
280319
"""
281320
A single interface used to gather metrics on a service. This class uses a regex to combine
282321
measures that match a pattern. For example, if you have a REST API, instead of defining
@@ -348,6 +387,10 @@ def timer(key, tags=None):
348387
return _global_registry.timer(key, tags)
349388

350389

390+
def event(key, tags=None):
391+
return _global_registry.event(key, tags)
392+
393+
351394
def gauge(key, gauge=None, tags=None):
352395
return _global_registry.gauge(key=key, gauge=gauge, tags=tags)
353396

pyformance/reporters/carbon_reporter.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# -*- coding: utf-8 -*-
2+
import contextlib
3+
import pickle
24
import socket
3-
import sys
45
import struct
5-
import pickle
6-
import contextlib
6+
import sys
7+
78
from six import iteritems
89

910
from .reporter import Reporter
@@ -13,21 +14,20 @@
1314

1415

1516
class CarbonReporter(Reporter):
16-
1717
"""
1818
Carbon is the network daemon to collect metrics for Graphite
1919
"""
2020

2121
def __init__(
22-
self,
23-
registry=None,
24-
reporting_interval=5,
25-
prefix="",
26-
server=DEFAULT_CARBON_SERVER,
27-
port=DEFAULT_CARBON_PORT,
28-
socket_factory=socket.socket,
29-
clock=None,
30-
pickle_protocol=False,
22+
self,
23+
registry=None,
24+
reporting_interval=5,
25+
prefix="",
26+
server=DEFAULT_CARBON_SERVER,
27+
port=DEFAULT_CARBON_PORT,
28+
socket_factory=socket.socket,
29+
clock=None,
30+
pickle_protocol=False,
3131
):
3232
super(CarbonReporter, self).__init__(registry, reporting_interval, clock)
3333
self.prefix = prefix
@@ -55,7 +55,7 @@ def _collect_metrics(self, registry, timestamp=None):
5555
(timestamp, metric_value),
5656
)
5757
for metric_name, metric in iteritems(metrics)
58-
for metric_key, metric_value in iteritems(metric)
58+
for metric_key, metric_value in iteritems(metric) if metric_key != "events"
5959
],
6060
protocol=2,
6161
)
@@ -65,22 +65,34 @@ def _collect_metrics(self, registry, timestamp=None):
6565
metrics_data = []
6666
for metric_name, metric in iteritems(metrics):
6767
for metric_key, metric_value in iteritems(metric):
68-
metric_line = "%s%s.%s %s %s\n" % (
69-
self.prefix,
70-
metric_name,
71-
metric_key,
72-
metric_value,
73-
timestamp,
74-
)
75-
metrics_data.append(metric_line)
68+
if metric_key != "events":
69+
metric_line = "%s%s.%s %s %s\n" % (
70+
self.prefix,
71+
metric_name,
72+
metric_key,
73+
metric_value,
74+
timestamp,
75+
)
76+
metrics_data.append(metric_line)
77+
else:
78+
for event in metric_value:
79+
for field, value in event.values.items():
80+
metric_line = "%s%s.%s %s %s\n" % (
81+
self.prefix,
82+
metric_name,
83+
field,
84+
value,
85+
event.time,
86+
)
87+
88+
metrics_data.append(metric_line)
7689
result = "".join(metrics_data)
7790
if sys.version_info[0] > 2:
7891
return result.encode()
7992
return result
8093

8194

8295
class UdpCarbonReporter(CarbonReporter):
83-
8496
"""
8597
The default CarbonReporter uses TCP.
8698
This sub-class uses UDP instead which might be unreliable but it is faster
@@ -90,6 +102,6 @@ def report_now(self, registry=None, timestamp=None):
90102
metrics = self._collect_metrics(registry or self.registry, timestamp)
91103
if metrics:
92104
with contextlib.closing(
93-
self.socket_factory(socket.AF_INET, socket.SOCK_DGRAM)
105+
self.socket_factory(socket.AF_INET, socket.SOCK_DGRAM)
94106
) as sock:
95107
sock.sendto(metrics, (self.server, self.port))

pyformance/reporters/console_reporter.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import print_function
3-
import sys
3+
44
import datetime
5+
import sys
6+
57
from .reporter import Reporter
68

79

810
class ConsoleReporter(Reporter):
9-
1011
"""
1112
Show metrics in a human readable form.
1213
This is useful for debugging if you want to read the current state on the console.
1314
"""
1415

1516
def __init__(
16-
self, registry=None, reporting_interval=30, stream=sys.stderr, clock=None
17+
self, registry=None, reporting_interval=30, stream=sys.stderr, clock=None
1718
):
1819
super(ConsoleReporter, self).__init__(registry, reporting_interval, clock)
1920
self.stream = stream
@@ -33,8 +34,25 @@ def _collect_metrics(self, registry, timestamp=None):
3334
]
3435
for key in metrics.keys():
3536
values = metrics[key]
36-
metrics_data.append("%s:" % key)
37-
for value_key in values.keys():
38-
metrics_data.append("%20s = %s" % (value_key, values[value_key]))
37+
if values.keys() != {"events"}:
38+
metrics_data.append("%s:" % key)
39+
for value_key in values.keys():
40+
if value_key != "events":
41+
metrics_data.append("%20s = %s" % (value_key, values[value_key]))
3942
metrics_data.append("")
43+
44+
# Add events
45+
for key in metrics.keys():
46+
for event in metrics[key].get("events", []):
47+
dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=event.time)
48+
metrics_data.append("== %s ===================================" %
49+
dt.strftime("%Y-%m-%d %H:%M:%S"))
50+
51+
metrics_data.append("%s:" % key)
52+
53+
for field, value in event.values.items():
54+
metrics_data.append("%20s = %s" % (field, value))
55+
56+
metrics_data.append("")
57+
4058
return metrics_data

0 commit comments

Comments
 (0)