summaryrefslogtreecommitdiff
path: root/oslo_middleware/stats.py
blob: 11c4789085d006a6d1015c014233b4234f5a267a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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(r"/(v[0-9]{1}\.[0-9]{1})")
UUID_REGEX = re.compile(
    r'.*(\.[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(r'.*(\.[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)