Change-Id: I04bd29aff7da65093bf7e78c915133d9c7b21f33
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-12-02 10:51:18 +00:00
parent af3341a2e5
commit d36e00490b
12 changed files with 192 additions and 117 deletions

View File

@@ -1,35 +1,25 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
# Replaces or checks mixed line ending
- id: mixed-line-ending
args: ['--fix', 'lf']
exclude: '.*\.(svg)$'
# Forbid files which have a UTF-8 byte-order marker
- id: check-byte-order-marker
# Checks that non-binary executables have a proper shebang
- id: fix-byte-order-marker
- id: check-executables-have-shebangs
# Check for files that contain merge conflict strings.
- id: check-merge-conflict
# Check for debugger imports and py37+ breakpoint()
# calls in python source
- id: debug-statements
- id: check-yaml
files: .*\.(yaml|yml)$
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.7
hooks:
- id: ruff-check
args: ['--fix', '--unsafe-fixes']
- id: ruff-format
- repo: https://opendev.org/openstack/hacking
rev: 7.0.0
rev: 8.0.0
hooks:
- id: hacking
additional_dependencies: []
- repo: https://github.com/PyCQA/bandit
rev: 1.7.10
hooks:
- id: bandit
args: ['-x', 'tests']
- repo: https://github.com/asottile/pyupgrade
rev: v3.18.0
hooks:
- id: pyupgrade
args: [--py3-only]

View File

@@ -34,9 +34,7 @@ openstackdocs_bug_tag = ''
# sphinxcontrib.apidoc options
apidoc_module_dir = '../../oslo_metrics'
apidoc_output_dir = 'reference/api'
apidoc_excluded_paths = [
'tests'
]
apidoc_excluded_paths = ['tests']
# The suffix of source filenames.
source_suffix = '.rst'
@@ -70,16 +68,19 @@ html_theme = 'openstackdocs'
# html_static_path = ['static']
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
htmlhelp_basename = f'{project}doc'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'%s.tex' % project,
'%s Documentation' % project,
'OpenStack Foundation', 'manual'),
(
'index',
f'{project}.tex',
f'{project} Documentation',
'OpenStack Foundation',
'manual',
)
]
intersphinx_mapping = {

View File

@@ -30,18 +30,28 @@ from oslo_metrics import message_router
oslo_metrics_configs = [
cfg.StrOpt('metrics_socket_file',
default='/var/tmp/metrics_collector.sock', # nosec
help='Unix domain socket file to be used'
' to send rpc related metrics'),
cfg.PortOpt('prometheus_port', default=3000,
help='Port number to expose metrics in prometheus format.'),
cfg.IntOpt('metrics_socket_perm', default=0o660,
help='Permission set to the unix domain socket file'),
cfg.BoolOpt('wsgi_silent_server', default=True,
help='Whether to silence the WSGI server. If disabled, the '
'WSGI server will print all requests it receives on '
'STDOUT. This could be very verbose.'),
cfg.StrOpt(
'metrics_socket_file',
default='/var/tmp/metrics_collector.sock', # noqa: S108
help='Unix domain socket file to be used to send rpc related metrics',
),
cfg.PortOpt(
'prometheus_port',
default=3000,
help='Port number to expose metrics in prometheus format.',
),
cfg.IntOpt(
'metrics_socket_perm',
default=0o660,
help='Permission set to the unix domain socket file',
),
cfg.BoolOpt(
'wsgi_silent_server',
default=True,
help='Whether to silence the WSGI server. If disabled, the '
'WSGI server will print all requests it receives on '
'STDOUT. This could be very verbose.',
),
]
cfg.CONF.register_opts(oslo_metrics_configs, group='oslo_metrics')
@@ -52,8 +62,7 @@ logging.register_options(CONF)
logging.setup(CONF, 'oslo-metrics')
class MetricsListener():
class MetricsListener:
def __init__(self, socket_path):
self.socket_path = socket_path
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
@@ -73,7 +82,8 @@ class MetricsListener():
def serve(self):
while self.start:
readable, writable, exceptional = select.select(
[self.socket], [], [], 1)
[self.socket], [], [], 1
)
if len(readable) == 0:
continue
try:
@@ -82,7 +92,7 @@ class MetricsListener():
msg = self.socket.recv(65565)
LOG.debug("got message")
self.router.process(msg)
except socket.timeout:
except TimeoutError:
pass
def stop(self):
@@ -122,8 +132,12 @@ def main():
try:
global httpd
if cfg.CONF.oslo_metrics.wsgi_silent_server:
httpd = make_server('', CONF.oslo_metrics.prometheus_port, app,
handler_class=_SilentHandler)
httpd = make_server(
'',
CONF.oslo_metrics.prometheus_port,
app,
handler_class=_SilentHandler,
)
else:
httpd = make_server('', CONF.oslo_metrics.prometheus_port, app)
signal.signal(signal.SIGTERM, handle_sigterm)

View File

@@ -21,13 +21,10 @@ from oslo_metrics import message_type
LOG = logging.getLogger(__name__)
MODULE_LISTS = [
"oslo_metrics.metrics.oslo_messaging",
]
MODULE_LISTS = ["oslo_metrics.metrics.oslo_messaging"]
class MessageRouter():
class MessageRouter:
def __init__(self):
self.modules = {}
for m_str in MODULE_LISTS:
@@ -61,8 +58,11 @@ class MessageRouter():
metric_with_label = getattr(metric_definition, "labels")
metric_with_label = metric_with_label(**metric.labels)
except AttributeError as e:
LOG.error("Failed to load labels func from metrics %s: %s",
metric.name, e)
LOG.error(
"Failed to load labels func from metrics %s: %s",
metric.name,
e,
)
return
LOG.debug("Get labels with {}: {}", metric.name, metric.labels)
@@ -74,8 +74,15 @@ class MessageRouter():
else:
embed_action()
except AttributeError as e:
LOG.error("Failed to perform metric actionv %s, %s: %s",
metric.action.action, metric.action.value, e)
LOG.error(
"Failed to perform metric actionv %s, %s: %s",
metric.action.action,
metric.action.value,
e,
)
return
LOG.debug("Perform action %s for %s metrics",
metric.action.action, metric.name)
LOG.debug(
"Perform action %s for %s metrics",
metric.action.action,
metric.name,
)

View File

@@ -26,13 +26,14 @@ class MetricValidationError(Exception):
self.message = message
class MetricAction():
class MetricAction:
actions = ['inc', 'observe']
def __init__(self, action, value):
if action not in self.actions:
raise UnSupportedMetricActionError(
"%s action is not supported" % action)
f"{action} action is not supported"
)
self.action = action
self.value = value
@@ -44,17 +45,15 @@ class MetricAction():
raise MetricValidationError("action need 'action' field")
if metric_action_dict["action"] not in cls.actions:
raise MetricValidationError(
"action should be choosen from %s" % cls.actions)
f"action should be choosen from {cls.actions}"
)
@classmethod
def from_dict(cls, metric_action_dict):
return cls(
metric_action_dict["action"],
metric_action_dict["value"]
)
return cls(metric_action_dict["action"], metric_action_dict["value"])
class Metric():
class Metric:
def __init__(self, module, name, action, **labels):
self.module = module
self.name = name
@@ -67,9 +66,9 @@ class Metric():
"name": self.name,
"action": {
"value": self.action.value,
"action": self.action.action
"action": self.action.action,
},
"labels": self.labels
"labels": self.labels,
}
return json.dumps(raw)
@@ -81,7 +80,8 @@ class Metric():
metric_dict["module"],
metric_dict["name"],
MetricAction.from_dict(metric_dict["action"]),
**metric_dict["labels"])
**metric_dict["labels"],
)
@classmethod
def _validate(cls, metric_dict):

View File

@@ -16,17 +16,46 @@
import prometheus_client
rpc_server_common_labels = [
'exchange', 'topic', 'server', 'endpoint', 'namespace',
'version', 'method', 'process'
'exchange',
'topic',
'server',
'endpoint',
'namespace',
'version',
'method',
'process',
]
rpc_client_common_labels = [
'call_type', 'exchange', 'topic', 'namespace', 'version',
'server', 'fanout', 'process', 'method', 'timeout'
'call_type',
'exchange',
'topic',
'namespace',
'version',
'server',
'fanout',
'process',
'method',
'timeout',
]
rpc_processing_seconds_buckets = [
0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0,
2.5, 5.0, 7.5, 10.0, 25.0, 50.0, 75.0, 100
0.01,
0.025,
0.05,
0.075,
0.1,
0.25,
0.5,
0.75,
1.0,
2.5,
5.0,
7.5,
10.0,
25.0,
50.0,
75.0,
100,
]
# RPC Server Metrics
@@ -34,42 +63,50 @@ rpc_server_invocation_start_total = prometheus_client.Counter(
'oslo_messaging_rpc_server_invocation_start_total',
'Total number of RPC invocation start. This doesn\'t count'
'if rpc server failed to find method from endpoints.',
rpc_server_common_labels)
rpc_server_common_labels,
)
rpc_server_invocation_end_total = prometheus_client.Counter(
'oslo_messaging_rpc_server_invocation_end_total',
'Total number of RPC invocation end.',
rpc_server_common_labels)
rpc_server_common_labels,
)
rpc_server_processing_seconds = prometheus_client.Histogram(
'oslo_messaging_rpc_server_processing_seconds',
'Duration of RPC processing.',
rpc_server_common_labels,
buckets=rpc_processing_seconds_buckets)
buckets=rpc_processing_seconds_buckets,
)
rpc_server_exception_total = prometheus_client.Counter(
'oslo_messaging_rpc_server_exception_total',
'Total number of exception while RPC processing.',
rpc_server_common_labels + ['exception'])
rpc_server_common_labels + ['exception'],
)
# RPC Client Metrics
rpc_client_invocation_start_total = prometheus_client.Counter(
'oslo_messaging_rpc_client_invocation_start_total',
'Total number of RPC invocation start.',
rpc_client_common_labels)
rpc_client_common_labels,
)
rpc_client_invocation_end_total = prometheus_client.Counter(
'oslo_messaging_rpc_client_invocation_end_total',
'Total number of RPC invocation end.',
rpc_client_common_labels)
rpc_client_common_labels,
)
rpc_client_processing_seconds = prometheus_client.Histogram(
'oslo_messaging_rpc_client_processing_seconds',
'Duration of RPC processing.',
rpc_client_common_labels,
buckets=rpc_processing_seconds_buckets)
buckets=rpc_processing_seconds_buckets,
)
rpc_client_exception_total = prometheus_client.Counter(
'oslo_messaging_rpc_client_exception_total',
'Total number of exception while RPC processing.',
rpc_client_common_labels + ['exception', ])
rpc_client_common_labels + ['exception'],
)

View File

@@ -24,7 +24,6 @@ import prometheus_client
class TestProcessMessage(base.BaseTestCase):
def setUp(self):
super().setUp()
@@ -48,9 +47,7 @@ class TestProcessMessage(base.BaseTestCase):
}
}"""
with mock.patch.object(
prometheus_client.Counter, 'inc',
) as mock_inc:
with mock.patch.object(prometheus_client.Counter, 'inc') as mock_inc:
router = message_router.MessageRouter()
router.process(received_json)
mock_inc.assert_called_once_with()
@@ -78,7 +75,7 @@ class TestProcessMessage(base.BaseTestCase):
}"""
with mock.patch.object(
prometheus_client.Histogram, 'observe',
prometheus_client.Histogram, 'observe'
) as mock_inc:
router = message_router.MessageRouter()
router.process(received_json)

View File

@@ -33,37 +33,44 @@ class TestMetricValidation(base.BaseTestCase):
self.assertEqual(message, e.message)
def test_message_validation(self):
metric = dict()
metric = {}
message = "module should be specified"
self.assertRaisesWithMessage(
message, message_type.Metric.from_json, json.dumps(metric))
message, message_type.Metric.from_json, json.dumps(metric)
)
metric['module'] = "test"
message = "name should be specified"
self.assertRaisesWithMessage(
message, message_type.Metric.from_json, json.dumps(metric))
message, message_type.Metric.from_json, json.dumps(metric)
)
metric['name'] = "test"
message = "action should be specified"
self.assertRaisesWithMessage(
message, message_type.Metric.from_json, json.dumps(metric))
message, message_type.Metric.from_json, json.dumps(metric)
)
metric['action'] = "test"
message = "labels should be specified"
self.assertRaisesWithMessage(
message, message_type.Metric.from_json, json.dumps(metric))
message, message_type.Metric.from_json, json.dumps(metric)
)
metric['labels'] = "test_label"
message = "action need 'value' field"
self.assertRaisesWithMessage(
message, message_type.Metric.from_json, json.dumps(metric))
message, message_type.Metric.from_json, json.dumps(metric)
)
metric['action'] = {"value": "1"}
message = "action need 'action' field"
self.assertRaisesWithMessage(
message, message_type.Metric.from_json, json.dumps(metric))
message, message_type.Metric.from_json, json.dumps(metric)
)
metric['action']['action'] = "test"
message = "action should be choosen from ['inc', 'observe']"
self.assertRaisesWithMessage(
message, message_type.Metric.from_json, json.dumps(metric))
message, message_type.Metric.from_json, json.dumps(metric)
)

View File

@@ -36,3 +36,21 @@ Repository = "https://opendev.org/openstack/oslo.metrics"
[tool.setuptools]
packages = ["oslo_metrics"]
[tool.ruff]
line-length = 79
[tool.ruff.lint]
select = ["C4", "E4", "E5", "E7", "E9", "F", "S", "UP"]
ignore = [
# we only use asserts for type narrowing
"S101",
]
[tool.ruff.lint.per-file-ignores]
"oslo_metrics/tests/*" = ["S"]
[tool.ruff.format]
quote-style = "preserve"
docstring-code-format = true
skip-magic-trailing-comma = true

View File

@@ -33,10 +33,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'openstackdocstheme',
'reno.sphinxext',
]
extensions = ['openstackdocstheme', 'reno.sphinxext']
# openstackdocstheme options
openstackdocs_repo_name = 'openstack/oslo.metrics'
@@ -194,10 +191,8 @@ htmlhelp_basename = 'oslo.metricsReleaseNotesDoc'
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
@@ -206,9 +201,13 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'oslo.metricsReleaseNotes.tex',
'oslo.metrics Release Notes Documentation',
'oslo.metrics Developers', 'manual'),
(
'index',
'oslo.metricsReleaseNotes.tex',
'oslo.metrics Release Notes Documentation',
'oslo.metrics Developers',
'manual',
)
]
# The name of an image file (relative to this directory) to place at the top of
@@ -236,9 +235,13 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'oslo.metricsReleaseNotes',
'oslo.metrics Release Notes Documentation',
['oslo.metrics Developers'], 1)
(
'index',
'oslo.metricsReleaseNotes',
'oslo.metrics Release Notes Documentation',
['oslo.metrics Developers'],
1,
)
]
# If true, show URL addresses after external links.
@@ -250,11 +253,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'oslo.metricsReleaseNotes',
'oslo.metrics Release Notes Documentation',
'oslo.metrics Developers', 'oslo.metricsReleaseNotes',
'One line description of project.',
'Miscellaneous'),
(
'index',
'oslo.metricsReleaseNotes',
'oslo.metrics Release Notes Documentation',
'oslo.metrics Developers',
'oslo.metricsReleaseNotes',
'One line description of project.',
'Miscellaneous',
)
]
# Documents to append as an appendix to all manuals.

View File

@@ -15,6 +15,4 @@
import setuptools
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)
setuptools.setup(setup_requires=['pbr>=2.0.0'], pbr=True)

View File

@@ -41,12 +41,11 @@ commands =
coverage xml -o cover/coverage.xml
[flake8]
# We only enable the hacking (H) checks
select = H
show-source = True
# H904: Delay string interpolations at logging calls
enable-extensions = H904
ignore = H405,W504,F405
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
[hacking]
import_exceptions =