Skip to content

Commit 947d0b0

Browse files
committed
Added support for breakdown metrics (#535)
closes #535 closes #479
1 parent c8f964d commit 947d0b0

File tree

9 files changed

+614
-78
lines changed

9 files changed

+614
-78
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
## Other changes
1010

11+
* Added support for recording breakdown metrics (#535)
1112
* Added instrumentation for `urllib2` (Python 2) / `urllib.request` (Python 3) (#464)
1213
* Added `disable_metrics` setting (#399)
1314
* Updated elasticsearch instrumentation for 7.x (#482, #483)

elasticapm/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def __init__(self, config=None, **inline):
161161
max_spans=self.config.transaction_max_spans,
162162
span_frames_min_duration=self.config.span_frames_min_duration,
163163
ignore_patterns=self.config.transactions_ignore_patterns,
164+
agent=self,
164165
)
165166
self.include_paths_re = stacks.get_path_regex(self.config.include_paths) if self.config.include_paths else None
166167
self.exclude_paths_re = stacks.get_path_regex(self.config.exclude_paths) if self.config.exclude_paths else None
@@ -169,6 +170,8 @@ def __init__(self, config=None, **inline):
169170
)
170171
for path in self.config.metrics_sets:
171172
self._metrics.register(path)
173+
if self.config.breakdown_metrics:
174+
self._metrics.register("elasticapm.metrics.sets.breakdown.BreakdownMetricSet")
172175
compat.atexit_register(self.close)
173176

174177
def get_handler(self, name):

elasticapm/conf/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,20 @@ class Config(_ConfigBase):
248248
"elasticapm.processors.sanitize_http_request_body",
249249
],
250250
)
251-
metrics_sets = _ListConfigValue("METRICS_SETS", default=["elasticapm.metrics.sets.cpu.CPUMetricSet"])
251+
metrics_sets = _ListConfigValue(
252+
"METRICS_SETS",
253+
default=[
254+
"elasticapm.metrics.sets.cpu.CPUMetricSet",
255+
"elasticapm.metrics.sets.transactions.TransactionsMetricSet",
256+
],
257+
)
252258
metrics_interval = _ConfigValue(
253259
"METRICS_INTERVAL",
254260
type=int,
255261
validators=[duration_validator, ExcludeRangeValidator(1, 999, "{range_start} - {range_end} ms")],
256262
default=30000,
257263
)
264+
breakdown_metrics = _BoolConfigValue("BREAKDOWN_METRICS", default=True)
258265
disable_metrics = _ListConfigValue("DISABLE_METRICS", type=starmatch_to_regex, default=[])
259266
api_request_size = _ConfigValue("API_REQUEST_SIZE", type=int, validators=[size_validator], default=750 * 1024)
260267
api_request_time = _ConfigValue("API_REQUEST_TIME", type=int, validators=[duration_validator], default=10 * 1000)

elasticapm/metrics/base_metrics.py

Lines changed: 116 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ def register(self, class_path):
7878
except ImportError as e:
7979
logger.warning("Could not register %s metricset: %s", class_path, compat.text_type(e))
8080

81+
def get_metricset(self, class_path):
82+
try:
83+
return self._metricsets[class_path]
84+
except KeyError:
85+
raise MetricSetNotFound(class_path)
86+
8187
def collect(self):
8288
"""
8389
Collect metrics from all registered metric sets and queues them for sending
@@ -104,48 +110,64 @@ def _stop_collect_timer(self):
104110
class MetricsSet(object):
105111
def __init__(self, registry):
106112
self._lock = threading.Lock()
107-
self._counters = {}
108-
self._gauges = {}
113+
self._counters = defaultdict(dict)
114+
self._gauges = defaultdict(dict)
115+
self._timers = defaultdict(dict)
109116
self._registry = registry
110117

111-
def counter(self, name, **labels):
118+
def counter(self, name, reset_on_collect=False, **labels):
112119
"""
113120
Returns an existing or creates and returns a new counter
114121
:param name: name of the counter
122+
:param reset_on_collect: indicate if the counter should be reset to 0 when collecting
115123
:param labels: a flat key/value map of labels
116124
:return: the counter object
117125
"""
118-
labels = self._labels_to_key(labels)
119-
key = (name, labels)
120-
with self._lock:
121-
if key not in self._counters:
122-
if self._registry._ignore_patterns and any(
123-
pattern.match(name) for pattern in self._registry._ignore_patterns
124-
):
125-
counter = noop_metric
126-
else:
127-
counter = Counter(name)
128-
self._counters[key] = counter
129-
return self._counters[key]
126+
return self._metric(self._counters, Counter, name, reset_on_collect, labels)
130127

131-
def gauge(self, name, **labels):
128+
def gauge(self, name, reset_on_collect=False, **labels):
132129
"""
133130
Returns an existing or creates and returns a new gauge
134131
:param name: name of the gauge
132+
:param reset_on_collect: indicate if the gouge should be reset to 0 when collecting
133+
:param labels: a flat key/value map of labels
135134
:return: the gauge object
136135
"""
136+
return self._metric(self._gauges, Gauge, name, reset_on_collect, labels)
137+
138+
def timer(self, name, reset_on_collect=False, **labels):
139+
"""
140+
Returns an existing or creates and returns a new timer
141+
:param name: name of the timer
142+
:param reset_on_collect: indicate if the timer should be reset to 0 when collecting
143+
:param labels: a flat key/value map of labels
144+
:return: the timer object
145+
"""
146+
return self._metric(self._timers, Timer, name, reset_on_collect, labels)
147+
148+
def _metric(self, container, metric_class, name, reset_on_collect, labels):
149+
"""
150+
Returns an existing or creates and returns a metric
151+
:param container: the container for the metric
152+
:param metric_class: the class of the metric
153+
:param name: name of the metric
154+
:param reset_on_collect: indicate if the metric should be reset to 0 when collecting
155+
:param labels: a flat key/value map of labels
156+
:return: the metric object
157+
"""
158+
137159
labels = self._labels_to_key(labels)
138160
key = (name, labels)
139161
with self._lock:
140-
if key not in self._gauges:
162+
if key not in container:
141163
if self._registry._ignore_patterns and any(
142164
pattern.match(name) for pattern in self._registry._ignore_patterns
143165
):
144-
gauge = noop_metric
166+
metric = noop_metric
145167
else:
146-
gauge = Gauge(name)
147-
self._gauges[key] = gauge
148-
return self._gauges[key]
168+
metric = metric_class(name, reset_on_collect=reset_on_collect)
169+
container[key] = metric
170+
return container[key]
149171

150172
def collect(self):
151173
"""
@@ -166,16 +188,28 @@ def collect(self):
166188
for (name, labels), c in compat.iteritems(self._counters):
167189
if c is not noop_metric:
168190
samples[labels].update({name: {"value": c.val}})
191+
if c.reset_on_collect:
192+
c.reset()
169193
if self._gauges:
170194
for (name, labels), g in compat.iteritems(self._gauges):
171195
if g is not noop_metric:
172196
samples[labels].update({name: {"value": g.val}})
197+
if g.reset_on_collect:
198+
g.reset()
199+
if self._timers:
200+
for (name, labels), t in compat.iteritems(self._timers):
201+
if t is not noop_metric:
202+
val, count = t.val
203+
samples[labels].update({name + ".sum.us": {"value": int(val * 1000000)}})
204+
samples[labels].update({name + ".count": {"value": count}})
205+
if t.reset_on_collect:
206+
t.reset()
173207
if samples:
174208
for labels, sample in compat.iteritems(samples):
175209
result = {"samples": sample, "timestamp": timestamp}
176210
if labels:
177211
result["tags"] = {k: v for k, v in labels}
178-
yield result
212+
yield self.before_yield(result)
179213

180214
def before_collect(self):
181215
"""
@@ -184,22 +218,39 @@ def before_collect(self):
184218
"""
185219
pass
186220

221+
def before_yield(self, data):
222+
return data
223+
187224
def _labels_to_key(self, labels):
188225
return tuple((k, compat.text_type(v)) for k, v in sorted(compat.iteritems(labels)))
189226

190227

228+
class SpanBoundMetricSet(MetricsSet):
229+
def before_yield(self, data):
230+
tags = data.get("tags", None)
231+
if tags:
232+
span_type, span_subtype = tags.pop("span.type", None), tags.pop("span.subtype", "")
233+
if span_type or span_subtype:
234+
data["span"] = {"type": span_type, "subtype": span_subtype}
235+
transaction_name, transaction_type = tags.pop("transaction.name", None), tags.pop("transaction.type", None)
236+
if transaction_name or transaction_type:
237+
data["transaction"] = {"name": transaction_name, "type": transaction_type}
238+
return data
239+
240+
191241
class Counter(object):
192-
__slots__ = ("label", "_lock", "_initial_value", "_val")
242+
__slots__ = ("name", "_lock", "_initial_value", "_val", "reset_on_collect")
193243

194-
def __init__(self, label, initial_value=0):
244+
def __init__(self, name, initial_value=0, reset_on_collect=False):
195245
"""
196246
Creates a new counter
197-
:param label: label of the counter
247+
:param name: name of the counter
198248
:param initial_value: initial value of the counter, defaults to 0
199249
"""
200-
self.label = label
250+
self.name = name
201251
self._lock = threading.Lock()
202252
self._val = self._initial_value = initial_value
253+
self.reset_on_collect = reset_on_collect
203254

204255
def inc(self, delta=1):
205256
"""
@@ -237,15 +288,16 @@ def val(self):
237288

238289

239290
class Gauge(object):
240-
__slots__ = ("label", "_val")
291+
__slots__ = ("name", "_val", "reset_on_collect")
241292

242-
def __init__(self, label):
293+
def __init__(self, name, reset_on_collect=False):
243294
"""
244295
Creates a new gauge
245-
:param label: label of the gauge
296+
:param name: label of the gauge
246297
"""
247-
self.label = label
298+
self.name = name
248299
self._val = None
300+
self.reset_on_collect = reset_on_collect
249301

250302
@property
251303
def val(self):
@@ -255,6 +307,35 @@ def val(self):
255307
def val(self, value):
256308
self._val = value
257309

310+
def reset(self):
311+
self._val = 0
312+
313+
314+
class Timer(object):
315+
__slots__ = ("name", "_val", "_count", "_lock", "reset_on_collect")
316+
317+
def __init__(self, name=None, reset_on_collect=False):
318+
self.name = name
319+
self._val = 0
320+
self._count = 0
321+
self._lock = threading.Lock()
322+
self.reset_on_collect = reset_on_collect
323+
324+
def update(self, duration, count=1):
325+
with self._lock:
326+
self._val += duration
327+
self._count += count
328+
329+
def reset(self):
330+
with self._lock:
331+
self._val = 0
332+
self._count = 0
333+
334+
@property
335+
def val(self):
336+
with self._lock:
337+
return self._val, self._count
338+
258339

259340
class NoopMetric(object):
260341
"""
@@ -285,3 +366,8 @@ def reset(self):
285366

286367

287368
noop_metric = NoopMetric("noop")
369+
370+
371+
class MetricSetNotFound(LookupError):
372+
def __init__(self, class_path):
373+
super(MetricSetNotFound, self).__init__("%s metric set not found" % class_path)

elasticapm/metrics/sets/breakdown.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2019, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
from __future__ import absolute_import
32+
33+
from elasticapm.metrics.base_metrics import SpanBoundMetricSet
34+
35+
36+
class BreakdownMetricSet(SpanBoundMetricSet):
37+
pass
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2019, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
from __future__ import absolute_import
32+
33+
from elasticapm.metrics.base_metrics import SpanBoundMetricSet
34+
35+
36+
class TransactionsMetricSet(SpanBoundMetricSet):
37+
pass

0 commit comments

Comments
 (0)