diff options
Diffstat (limited to 'oslo_middleware/stats.py')
-rw-r--r-- | oslo_middleware/stats.py | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/oslo_middleware/stats.py b/oslo_middleware/stats.py new file mode 100644 index 0000000..9839fd5 --- /dev/null +++ b/oslo_middleware/stats.py @@ -0,0 +1,131 @@ +# Copyright (c) 2016 Cisco Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import re + +import statsd +import webob.dec + +from oslo_middleware import base + +LOG = logging.getLogger(__name__) +VERSION_REGEX = re.compile("/(v[0-9]{1}\.[0-9]{1})") +UUID_REGEX = re.compile( + '.*(\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*a', + re.IGNORECASE) +# UUIDs without the - char, used in some places in Nova URLs. +SHORT_UUID_REGEX = re.compile('.*(\.[0-9a-fA-F]{32}).*') + + +class StatsMiddleware(base.ConfigurableMiddleware): + """Send stats to statsd based on API requests. + + Examines the URL path and request method, and sends a stat count and timer + to a statsd host based on the path/method. + + If your statsd is configured to send stats to Graphite, you'll end up with + stat names of the form:: + + timer.<appname>.<METHOD>.<path>.<from>.<url> + + Note that URLs with versions in them (pretty much all of Openstack) + are always processed to replace the dot with _, so for example v2.0 + becomes v2_0, and v1.1 becomes v1_1, since a dot '.' has special + meaning in Graphite. + + The original StatsD is written in nodejs. If you want a Python + implementation, install Bucky instead as it's a drop-in replacement + (and much nicer IMO). + + The Paste config must contain some parameters. Configure a filter like + this:: + + [filter:stats] + paste.filter_factory = oslo_middleware.stats:StatsMiddleware.factory + name = my_application_name # e.g. 'glance' + stats_host = my_statsd_host.example.com + # Optional args to further process the stat name that's generated: + remove_uuid = True + remove_short_uuid = True + # The above uuid processing is required in, e.g. Nova, if you want to + # collect generic stats rather than one per server instance. + """ + + def __init__(self, application, conf): + super(StatsMiddleware, self).__init__(application, conf) + self.application = application + self.stat_name = conf.get('name') + if self.stat_name is None: + raise AttributeError('name must be specified') + self.stats_host = conf.get('stats_host') + if self.stats_host is None: + raise AttributeError('stats_host must be specified') + self.remove_uuid = conf.get('remove_uuid', False) + self.remove_short_uuid = conf.get('remove_short_uuid', False) + self.statsd = statsd.StatsClient(self.stats_host) + + @staticmethod + def strip_short_uuid(path): + """Remove short-form UUID from supplied path. + + Only call after replacing slashes with dots in path. + """ + match = SHORT_UUID_REGEX.match(path) + if match is None: + return path + return path.replace(match.group(1), '') + + @staticmethod + def strip_uuid(path): + """Remove normal-form UUID from supplied path. + + Only call after replacing slashes with dots in path. + """ + match = UUID_REGEX.match(path) + if match is None: + return path + return path.replace(match.group(1), '') + + @staticmethod + def strip_dot_from_version(path): + # Replace vN.N with vNN. + match = VERSION_REGEX.match(path) + if match is None: + return path + return path.replace(match.group(1), match.group(1).replace('.', '')) + + @webob.dec.wsgify + def __call__(self, request): + path = request.path + path = self.strip_dot_from_version(path) + + # Remove leading slash, if any, so we can be sure of the number + # of dots just below. + path = path.lstrip('/') + + stat = "{name}.{method}".format( + name=self.stat_name, method=request.method) + if path != '': + stat += '.' + path.replace('/', '.') + + if self.remove_short_uuid: + stat = self.strip_short_uuid(stat) + + if self.remove_uuid: + stat = self.strip_uuid(stat) + + LOG.debug("Incrementing stat count %s", stat) + with self.statsd.timer(stat): + return request.get_response(self.application) |