File: multiprocess.py

package info (click to toggle)
prometheus-flask-exporter 0.23.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 896 kB
  • sloc: python: 2,889; sh: 709; makefile: 4
file content (241 lines) | stat: -rw-r--r-- 8,417 bytes parent folder | download
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import os
from abc import ABCMeta, abstractmethod

from prometheus_client import CollectorRegistry
from prometheus_client import start_http_server as pc_start_http_server
from prometheus_client.multiprocess import MultiProcessCollector
from prometheus_client.multiprocess import mark_process_dead as pc_mark_process_dead

from . import PrometheusMetrics


def _check_multiproc_env_var():
    """
    Checks that the `PROMETHEUS_MULTIPROC_DIR` environment variable is set,
    which is required for the multiprocess collector to work properly.

    :raises ValueError: if the environment variable is not set
        or if it does not point to a directory
    """

    if 'PROMETHEUS_MULTIPROC_DIR' in os.environ:
        if os.path.isdir(os.environ['PROMETHEUS_MULTIPROC_DIR']):
            return
    elif 'prometheus_multiproc_dir' in os.environ:
        if os.path.isdir(os.environ['prometheus_multiproc_dir']):
            return

    raise ValueError('one of env PROMETHEUS_MULTIPROC_DIR or env prometheus_multiproc_dir ' +
        'must be set and be a directory')


class MultiprocessPrometheusMetrics(PrometheusMetrics):
    """
    An extension of the `PrometheusMetrics` class that provides
    convenience functions for multiprocess applications.

    There are ready to use classes for uWSGI and Gunicorn.
    For everything else, extend this class and override
    the `should_start_http_server` to only return `True`
    from one process only - typically the main one.

    Note: you will need to explicitly call the `start_http_server` function.
    """

    __metaclass__ = ABCMeta

    def __init__(self, app=None, **kwargs):
        """
        Create a new multiprocess-aware Prometheus metrics export configuration.

        :param registry: the Prometheus Registry to use (can be `None` and it
            will be registered with `prometheus_client.multiprocess.MultiProcessCollector`)
        """

        _check_multiproc_env_var()

        registry = kwargs.pop('registry', CollectorRegistry())
        MultiProcessCollector(registry)

        kwargs.pop('path', None)  # remove the path parameter if it was passed in

        super().__init__(
            app=app, path=None, registry=registry, **kwargs
        )

    def start_http_server(self, port, host='0.0.0.0', endpoint=None, ssl=None):
        """
        Start an HTTP server for exposing the metrics, if the
        `should_start_http_server` function says we should, otherwise just return.
        Uses the implementation from `prometheus_client` rather than a Flask app.

        :param port: the HTTP port to expose the metrics endpoint on
        :param host: the HTTP host to listen on (default: `0.0.0.0`)
        :param endpoint: **ignored**, the HTTP server will respond on any path
        :param ssl: **ignored**, the server will not support SSL/HTTPS
        """

        if self.should_start_http_server():
            pc_start_http_server(port, host, registry=self.registry)

    @abstractmethod
    def should_start_http_server(self):
        """
        Whether or not to start the HTTP server.
        Only return `True` from one process only, typically the main one.

        Note: you still need to explicitly call the `start_http_server` function.

        :return: `True` if the server should start, `False` otherwise
        """

        pass


class MultiprocessInternalPrometheusMetrics(MultiprocessPrometheusMetrics):
    """
    A multiprocess `PrometheusMetrics` extension with the metrics endpoint
    registered on the Flask app internally.
    This variant is expected to expose the metrics endpoint on the same server
    as the production endpoints are served too.

    Alternatively, you can use the instance functions as well.
    """

    def __init__(self, app=None, path='/metrics', **kwargs):
        """
        Create a new multiprocess-aware Prometheus metrics export configuration.
        """

        super().__init__(app=app, **kwargs)

        if app:
            self.register_endpoint(path)
        else:
            self.path = path

    def should_start_http_server(self):
        return False

    @classmethod
    def start_http_server_when_ready(cls, port, host='0.0.0.0'):
        import warnings
        warnings.warn(
            'The `MultiprocessInternalPrometheusMetrics` class is expected to expose the metrics endpoint '
            'on the same Flask application, so the `start_http_server_when_ready` should not be called.',
            UserWarning
        )


class UWsgiPrometheusMetrics(MultiprocessPrometheusMetrics):
    """
    A multiprocess `PrometheusMetrics` extension targeting uWSGI deployments.
    This will only start the HTTP server for metrics on the main process,
    indicated by `uwsgi.masterpid()`.
    """

    def should_start_http_server(self):
        import uwsgi
        return os.getpid() == uwsgi.masterpid()


class GunicornPrometheusMetrics(MultiprocessPrometheusMetrics):
    """
    A multiprocess `PrometheusMetrics` extension targeting Gunicorn deployments.
    This variant is expected to serve the metrics endpoint on an individual HTTP server.
    See `GunicornInternalPrometheusMetrics` for one that serves the metrics endpoint
    on the same server as the other endpoints.

    It should have Gunicorn configuration to start the HTTP server like this:

        from prometheus_flask_exporter.multiprocess import GunicornPrometheusMetrics

        def when_ready(server):
            GunicornPrometheusMetrics.start_http_server_when_ready(metrics_port)

        def child_exit(server, worker):
            GunicornPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid)

    Alternatively, you can use the instance functions as well.
    """

    def should_start_http_server(self):
        return True

    @classmethod
    def start_http_server_when_ready(cls, port, host='0.0.0.0'):
        """
        Start the HTTP server from the Gunicorn config module.
        Doesn't necessarily need an instance, a class is fine.

        Example:

            def when_ready(server):
                GunicornPrometheusMetrics.start_http_server_when_ready(metrics_port)

        :param port: the HTTP port to expose the metrics endpoint on
        :param host: the HTTP host to listen on (default: `0.0.0.0`)
        """

        _check_multiproc_env_var()

        GunicornPrometheusMetrics().start_http_server(port, host)

    @classmethod
    def mark_process_dead_on_child_exit(cls, pid):
        """
        Mark a child worker as exited from the Gunicorn config module.

        Example:

            def child_exit(server, worker):
                GunicornPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid)

        :param pid: the worker pid that has exited
        """

        pc_mark_process_dead(pid)


class GunicornInternalPrometheusMetrics(GunicornPrometheusMetrics):
    """
    A multiprocess `PrometheusMetrics` extension targeting Gunicorn deployments.
    This variant is expected to expose the metrics endpoint on the same server
    as the production endpoints are served too.
    See also the `GunicornPrometheusMetrics` class that will start a
    new HTTP server for the metrics endpoint.

    It should have Gunicorn configuration to start the HTTP server like this:

        from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics

        def child_exit(server, worker):
            GunicornInternalPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid)

    Alternatively, you can use the instance functions as well.
    """

    def __init__(self, app=None, path='/metrics', **kwargs):
        """
        Create a new multiprocess-aware Prometheus metrics export configuration.
        """

        super().__init__(app=app, **kwargs)

        if app:
            self.register_endpoint(path)
        else:
            self.path = path

    def should_start_http_server(self):
        return False

    @classmethod
    def start_http_server_when_ready(cls, port, host='0.0.0.0'):
        import warnings
        warnings.warn(
            'The `GunicornInternalPrometheusMetrics` class is expected to expose the metrics endpoint '
            'on the same Flask application, so the `start_http_server_when_ready` should not be called. '
            'Maybe you are looking for the `GunicornPrometheusMetrics` class?',
            UserWarning
        )