Skip to content

Prometheus Metrics #1447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a1432c2
Add metrics server endpoint
abhinavsingh Aug 10, 2024
17902ca
Merge branch 'develop' into metrics
abhinavsingh Aug 10, 2024
1622249
Setup metrics subscriber
abhinavsingh Aug 10, 2024
ce91b96
`MetricsSubscriber` as context manager
abhinavsingh Aug 10, 2024
dba32fa
Fix lint issues
abhinavsingh Aug 10, 2024
56fbf91
`--enable-metrics` flag which setup Metrics subscriber, collector and…
abhinavsingh Aug 10, 2024
c512355
Use file storage based mechanism to share internal metrics with prome…
abhinavsingh Aug 11, 2024
77b9360
Lint fixes
abhinavsingh Aug 11, 2024
d3e72e4
Move `_setup_metrics_directory` within subscriber which only run once
abhinavsingh Aug 11, 2024
3b39019
Use global `metrics_lock` via flags
abhinavsingh Aug 11, 2024
f63cbfb
Remove top-level imports for prometheus_client
abhinavsingh Aug 11, 2024
8b9792e
Add `requirements-metrics.txt`
abhinavsingh Aug 11, 2024
bc82aa9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 11, 2024
6cd7933
Fix typo in makefile
abhinavsingh Aug 11, 2024
e94154d
Fix typo
abhinavsingh Aug 11, 2024
626a461
fix type, lint, flake issues
abhinavsingh Aug 11, 2024
54762f8
Remove event queue prop
abhinavsingh Aug 11, 2024
022d14d
Fix typo
abhinavsingh Aug 11, 2024
c1077d4
Give any role to `proxy.http.server.metrics.get_collector`
abhinavsingh Aug 11, 2024
fb87a12
rtype
abhinavsingh Aug 11, 2024
975b6b6
`emit_request_complete` for web servers
abhinavsingh Aug 11, 2024
f16a528
Fix doc issues
abhinavsingh Aug 11, 2024
ec88ced
Refactor
abhinavsingh Aug 11, 2024
701ceb9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 11, 2024
8bd28af
Rename metrics to start with proxypy_work_
abhinavsingh Aug 11, 2024
faea962
Merge branch 'metrics' of github.com:abhinavsingh/proxy.py into metrics
abhinavsingh Aug 11, 2024
4e0bcfe
Startup `MetricsEventSubscriber` as part of proxy
abhinavsingh Aug 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ repos:
- cryptography==36.0.2; python_version <= '3.6'
- types-setuptools == 57.4.2
- pyyaml==5.3.1
# From requirements-metrics.txt
- prometheus_client==0.20.0
args:
# FIXME: get rid of missing imports ignore
- --ignore-missing-imports
Expand Down
1 change: 1 addition & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ python:
path: .
- requirements: requirements-tunnel.txt
- requirements: docs/requirements.txt
- requirements: requirements-metrics.txt

...
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ lib-dep:
pip install \
-r requirements-testing.txt \
-r requirements-release.txt \
-r requirements-tunnel.txt && \
-r requirements-tunnel.txt \
-r requirements-metrics.txt && \
pip install "setuptools>=42"

lib-pre-commit:
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --allow-unsafe --generate-hashes --output-file=docs/requirements.txt --strip-extras docs/requirements.in
Expand Down
4 changes: 4 additions & 0 deletions proxy/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ def _env_threadless_compliant() -> bool:
DEFAULT_OPEN_FILE_LIMIT = 1024
DEFAULT_PAC_FILE = None
DEFAULT_PAC_FILE_URL_PATH = b'/'
DEFAULT_ENABLE_METRICS = False
DEFAULT_METRICS_URL_PATH = b"/metrics"
DEFAULT_PID_FILE = None
DEFAULT_PORT_FILE = None
DEFAULT_PLUGINS: List[Any] = []
Expand Down Expand Up @@ -172,6 +174,7 @@ def _env_threadless_compliant() -> bool:
)
DEFAULT_CACHE_REQUESTS = False
DEFAULT_CACHE_BY_CONTENT_TYPE = False
DEFAULT_METRICS_DIRECTORY_PATH = os.path.join(DEFAULT_DATA_DIRECTORY_PATH, "metrics")

# Cor plugins enabled by default or via flags
DEFAULT_ABC_PLUGINS = [
Expand All @@ -190,6 +193,7 @@ def _env_threadless_compliant() -> bool:
PLUGIN_DEVTOOLS_PROTOCOL = 'proxy.http.inspector.devtools.DevtoolsProtocolPlugin'
PLUGIN_INSPECT_TRAFFIC = 'proxy.http.inspector.inspect_traffic.InspectTrafficPlugin'
PLUGIN_WEBSOCKET_TRANSPORT = 'proxy.http.websocket.transport.WebSocketTransport'
PLUGIN_METRICS = "proxy.http.server.MetricsWebServerPlugin"

PY2_DEPRECATION_MESSAGE = '''DEPRECATION: proxy.py no longer supports Python 2.7. Kindly upgrade to Python 3+. '
'If for some reasons you cannot upgrade, use'
Expand Down
27 changes: 21 additions & 6 deletions proxy/common/flag.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@
from .plugins import Plugins
from .version import __version__
from .constants import (
COMMA, PLUGIN_PAC_FILE, PLUGIN_DASHBOARD, PLUGIN_HTTP_PROXY,
PLUGIN_PROXY_AUTH, PLUGIN_WEB_SERVER, DEFAULT_NUM_WORKERS,
PLUGIN_REVERSE_PROXY, DEFAULT_NUM_ACCEPTORS, PLUGIN_INSPECT_TRAFFIC,
DEFAULT_DISABLE_HEADERS, PY2_DEPRECATION_MESSAGE, DEFAULT_DEVTOOLS_WS_PATH,
PLUGIN_DEVTOOLS_PROTOCOL, PLUGIN_WEBSOCKET_TRANSPORT,
DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_MIN_COMPRESSION_LENGTH,
COMMA, PLUGIN_METRICS, PLUGIN_PAC_FILE, PLUGIN_DASHBOARD,
PLUGIN_HTTP_PROXY, PLUGIN_PROXY_AUTH, PLUGIN_WEB_SERVER,
DEFAULT_NUM_WORKERS, PLUGIN_REVERSE_PROXY, DEFAULT_NUM_ACCEPTORS,
PLUGIN_INSPECT_TRAFFIC, DEFAULT_DISABLE_HEADERS, PY2_DEPRECATION_MESSAGE,
DEFAULT_DEVTOOLS_WS_PATH, PLUGIN_DEVTOOLS_PROTOCOL,
PLUGIN_WEBSOCKET_TRANSPORT, DEFAULT_DATA_DIRECTORY_PATH,
DEFAULT_MIN_COMPRESSION_LENGTH,
)


Expand Down Expand Up @@ -182,6 +183,13 @@
args.enable_events,
),
)
args.enable_metrics = cast(
bool,
opts.get(
'enable_metrics',
args.enable_metrics,
),
)

# Load default plugins along with user provided --plugins
default_plugins = [
Expand All @@ -195,6 +203,9 @@
default_plugins + auth_plugins + requested_plugins,
)

if bytes_(PLUGIN_METRICS) in default_plugins:
args.metrics_lock = multiprocessing.Lock()

Check warning on line 207 in proxy/common/flag.py

View check run for this annotation

Codecov / codecov/patch

proxy/common/flag.py#L207

Added line #L207 was not covered by tests

# https://github.com/python/mypy/issues/5865
#
# def option(t: object, key: str, default: Any) -> Any:
Expand Down Expand Up @@ -422,6 +433,10 @@
default_plugins.append(PLUGIN_INSPECT_TRAFFIC)
args.enable_events = True
args.enable_devtools = True
if hasattr(args, 'enable_metrics') and args.enable_metrics:
default_plugins.append(PLUGIN_WEB_SERVER)
default_plugins.append(PLUGIN_METRICS)
args.enable_events = True

Check warning on line 439 in proxy/common/flag.py

View check run for this annotation

Codecov / codecov/patch

proxy/common/flag.py#L437-L439

Added lines #L437 - L439 were not covered by tests
if hasattr(args, 'enable_devtools') and args.enable_devtools:
default_plugins.append(PLUGIN_DEVTOOLS_PROTOCOL)
default_plugins.append(PLUGIN_WEB_SERVER)
Expand Down
114 changes: 114 additions & 0 deletions proxy/core/event/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
"""
proxy.py
~~~~~~~~
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
Network monitoring, controls & Application development, testing, debugging.

:copyright: (c) 2013-present by Abhinav Singh and contributors.
:license: BSD, see LICENSE for more details.
"""
import os
import glob
from typing import Any, Dict
from pathlib import Path
from multiprocessing.synchronize import Lock

from ...core.event import EventQueue, EventSubscriber, eventNames
from ...common.constants import DEFAULT_METRICS_DIRECTORY_PATH


class MetricsStorage:

def __init__(self, lock: Lock) -> None:
self._lock = lock

def get_counter(self, name: str) -> float:
with self._lock:
return self._get_counter(name)

Check warning on line 28 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L28

Added line #L28 was not covered by tests

def _get_counter(self, name: str) -> float:
path = os.path.join(DEFAULT_METRICS_DIRECTORY_PATH, f'{name}.counter')

Check warning on line 31 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L31

Added line #L31 was not covered by tests
if not os.path.exists(path):
return 0
return float(Path(path).read_text(encoding='utf-8').strip())

Check warning on line 34 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L33-L34

Added lines #L33 - L34 were not covered by tests

def incr_counter(self, name: str, by: float = 1.0) -> None:
with self._lock:
self._incr_counter(name, by)

Check warning on line 38 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L38

Added line #L38 was not covered by tests

def _incr_counter(self, name: str, by: float = 1.0) -> None:
current = self._get_counter(name)
path = os.path.join(DEFAULT_METRICS_DIRECTORY_PATH, f'{name}.counter')
Path(path).write_text(str(current + by), encoding='utf-8')

Check warning on line 43 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L41-L43

Added lines #L41 - L43 were not covered by tests

def get_gauge(self, name: str) -> float:
with self._lock:
return self._get_gauge(name)

Check warning on line 47 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L47

Added line #L47 was not covered by tests

def _get_gauge(self, name: str) -> float:
path = os.path.join(DEFAULT_METRICS_DIRECTORY_PATH, f'{name}.gauge')

Check warning on line 50 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L50

Added line #L50 was not covered by tests
if not os.path.exists(path):
return 0
return float(Path(path).read_text(encoding='utf-8').strip())

Check warning on line 53 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L52-L53

Added lines #L52 - L53 were not covered by tests

def set_gauge(self, name: str, value: float) -> None:
"""Stores a single values."""
with self._lock:
self._set_gauge(name, value)

Check warning on line 58 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L58

Added line #L58 was not covered by tests

def _set_gauge(self, name: str, value: float) -> None:
path = os.path.join(DEFAULT_METRICS_DIRECTORY_PATH, f'{name}.gauge')

Check warning on line 61 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L61

Added line #L61 was not covered by tests
with open(path, 'w', encoding='utf-8') as g:
g.write(str(value))

Check warning on line 63 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L63

Added line #L63 was not covered by tests


class MetricsEventSubscriber:

def __init__(self, event_queue: EventQueue, metrics_lock: Lock) -> None:
"""Aggregates metric events pushed by proxy.py core and plugins.

1) Metrics are stored and managed by multiprocessing safe MetricsStorage
2) Collection must be done via MetricsWebServerPlugin endpoint
"""
self.storage = MetricsStorage(metrics_lock)
self.subscriber = EventSubscriber(
event_queue,
callback=lambda event: MetricsEventSubscriber.callback(self.storage, event),
)

def setup(self) -> None:
self._setup_metrics_directory()
self.subscriber.setup()

def shutdown(self) -> None:
self.subscriber.shutdown()

def __enter__(self) -> 'MetricsEventSubscriber':
self.setup()
return self

Check warning on line 89 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L88-L89

Added lines #L88 - L89 were not covered by tests

def __exit__(self, *args: Any) -> None:
self.shutdown()

Check warning on line 92 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L92

Added line #L92 was not covered by tests

@staticmethod
def callback(storage: MetricsStorage, event: Dict[str, Any]) -> None:
if event['event_name'] == eventNames.WORK_STARTED:
storage.incr_counter('work_started')

Check warning on line 97 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L97

Added line #L97 was not covered by tests
elif event['event_name'] == eventNames.REQUEST_COMPLETE:
storage.incr_counter('request_complete')

Check warning on line 99 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L99

Added line #L99 was not covered by tests
elif event['event_name'] == eventNames.WORK_FINISHED:
storage.incr_counter('work_finished')

Check warning on line 101 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L101

Added line #L101 was not covered by tests
else:
print('Unhandled', event)

Check warning on line 103 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L103

Added line #L103 was not covered by tests

def _setup_metrics_directory(self) -> None:
os.makedirs(DEFAULT_METRICS_DIRECTORY_PATH, exist_ok=True)
patterns = ['*.counter', '*.gauge']
for pattern in patterns:
files = glob.glob(os.path.join(DEFAULT_METRICS_DIRECTORY_PATH, pattern))
for file_path in files:
try:
os.remove(file_path)
except OSError as e:
print(f'Error deleting file {file_path}: {e}')

Check warning on line 114 in proxy/core/event/metrics.py

View check run for this annotation

Codecov / codecov/patch

proxy/core/event/metrics.py#L111-L114

Added lines #L111 - L114 were not covered by tests
2 changes: 2 additions & 0 deletions proxy/http/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""
from .web import HttpWebServerPlugin
from .plugin import ReverseProxyBasePlugin, HttpWebServerBasePlugin
from .metrics import MetricsWebServerPlugin
from .protocols import httpProtocolTypes
from .pac_plugin import HttpWebServerPacFilePlugin

Expand All @@ -20,4 +21,5 @@
'HttpWebServerBasePlugin',
'httpProtocolTypes',
'ReverseProxyBasePlugin',
'MetricsWebServerPlugin',
]
Loading
Loading