032addde67
The new StatsMiddleware is a Paste filter that examines the URL path and request method, and sends a stat count and a timer to a statsd host whose name is 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> Because a dot has special meaning in Graphite, dots in API versions that appear in the path will be replaced with _, so for example v2.1 becomes v2_1, and v1.0 becomes v1_0. Change-Id: Ieaffeded1bf81c0782d88f49b6f5209f11744899
132 lines
4.5 KiB
Python
132 lines
4.5 KiB
Python
# 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)
|