
Improvements: - better readability - get_prom_client_from_keystone can be used by watcher for PrometheusAPIClient initialization. - A better split between how the client is created for Aetos and how it's created for Prometheus. Changes to the previous behavior of get_prometheus_client: - It's not possible to override PrometheusAPIClient arguments with environment variables if they're retrieved from Keystone. - It's not possible to use config file / environment variables to connect to Aetos (the keystone session parameter won't be passed to PrometheusAPIClient and so it won't be able to authenticate) I think even though, there are some changes to how Aetos can access can be configured, it's getting it a bit closer to how other services work (getting the endpoint from keystone and not requiring / allowing additional configuration). Prometheus access configuration through config file / env variables stays unchanged. Change-Id: Icd08347056e92502992d4fee799bb3e06e03c0c9 Signed-off-by: Jaromir Wysoglad <jwysogla@redhat.com>
179 lines
5.7 KiB
Python
179 lines
5.7 KiB
Python
# Copyright 2023 Red Hat, Inc.
|
|
#
|
|
# 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 os
|
|
from urllib import parse
|
|
|
|
from keystoneauth1 import adapter
|
|
from keystoneauth1.exceptions import catalog as keystone_exception
|
|
from oslo_utils import netutils
|
|
import yaml
|
|
|
|
from observabilityclient.prometheus_client import PrometheusAPIClient
|
|
|
|
|
|
DEFAULT_CONFIG_LOCATIONS = (
|
|
[os.path.join(os.environ["HOME"], ".config/openstack/"), "/etc/openstack/"]
|
|
if "HOME" in os.environ
|
|
else ["/etc/openstack/"]
|
|
)
|
|
CONFIG_FILE_NAME = "prometheus.yaml"
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class ConfigurationError(Exception):
|
|
pass
|
|
|
|
|
|
def get_config_file():
|
|
if os.path.exists(CONFIG_FILE_NAME):
|
|
LOG.debug("Using %s as prometheus configuration", CONFIG_FILE_NAME)
|
|
return open(CONFIG_FILE_NAME, "r")
|
|
for path in DEFAULT_CONFIG_LOCATIONS:
|
|
full_filename = path + CONFIG_FILE_NAME
|
|
if os.path.exists(full_filename):
|
|
LOG.debug("Using %s as prometheus configuration", full_filename)
|
|
return open(full_filename, "r")
|
|
return None
|
|
|
|
|
|
def get_prom_client_from_keystone(session, adapter_options=None):
|
|
if adapter_options is None:
|
|
adapter_options = {}
|
|
endpoint = adapter.Adapter(
|
|
session=session, **adapter_options
|
|
).get_endpoint()
|
|
parsed_url = parse.urlparse(endpoint)
|
|
|
|
escaped_host = netutils.escape_ipv6(parsed_url.hostname)
|
|
root_path = parsed_url.path.strip('/')
|
|
tls = parsed_url.scheme == "https"
|
|
|
|
if parsed_url.port is not None:
|
|
url = f'{escaped_host}:{parsed_url.port}'
|
|
else:
|
|
url = escaped_host
|
|
|
|
client = PrometheusAPIClient(url, session, root_path)
|
|
|
|
if tls:
|
|
client.set_ca_cert(True)
|
|
return client
|
|
|
|
|
|
def get_prom_client_from_file_or_env():
|
|
host = port = ca_cert = None
|
|
root_path = ''
|
|
conf_file = get_config_file()
|
|
if conf_file is not None:
|
|
conf = yaml.safe_load(conf_file)
|
|
if 'host' in conf:
|
|
host = conf['host']
|
|
if 'port' in conf:
|
|
port = conf['port']
|
|
if 'ca_cert' in conf:
|
|
ca_cert = conf['ca_cert']
|
|
if 'root_path' in conf:
|
|
root_path = conf['root_path']
|
|
conf_file.close()
|
|
if 'PROMETHEUS_HOST' in os.environ:
|
|
host = os.environ['PROMETHEUS_HOST']
|
|
if 'PROMETHEUS_PORT' in os.environ:
|
|
port = os.environ['PROMETHEUS_PORT']
|
|
if 'PROMETHEUS_CA_CERT' in os.environ:
|
|
ca_cert = os.environ['PROMETHEUS_CA_CERT']
|
|
if 'PROMETHEUS_ROOT_PATH' in os.environ:
|
|
root_path = os.environ['PROMETHEUS_ROOT_PATH']
|
|
if host is None or port is None:
|
|
raise ConfigurationError("Can't find prometheus host and "
|
|
"port configuration in config file or "
|
|
"environment variables.")
|
|
escaped_host = netutils.escape_ipv6(host)
|
|
client = PrometheusAPIClient(
|
|
f"{escaped_host}:{port}", None, root_path
|
|
)
|
|
if ca_cert is not None:
|
|
client.set_ca_cert(ca_cert)
|
|
return client
|
|
|
|
|
|
def get_prometheus_client(session=None, adapter_options=None):
|
|
if adapter_options is None:
|
|
adapter_options = {}
|
|
|
|
keystone_error = None
|
|
if session is not None:
|
|
try:
|
|
return get_prom_client_from_keystone(session, adapter_options)
|
|
except keystone_exception.EndpointNotFound as e:
|
|
# NOTE(jwysogla): Fallback to get the endpoint configuration from
|
|
# the config file and env vars. If that doesn't work, the error
|
|
# message is raised as ConfigurationError later.
|
|
keystone_error = e
|
|
LOG.debug("Aetos endpoint discovery from Keystone failed: %s", e)
|
|
|
|
# NOTE(jwysogla): Always fallback to the original method of discovery
|
|
# through config file and env variables for backwards compatibility.
|
|
try:
|
|
return get_prom_client_from_file_or_env()
|
|
except ConfigurationError as e:
|
|
if keystone_error is not None:
|
|
raise ConfigurationError(
|
|
f"Failed to configure Prometheus client. "
|
|
f"Aetos discovery from keystone failed: '{keystone_error}'. "
|
|
f"Prometheus configuration from config file and environment "
|
|
f"variables failed: '{e}'"
|
|
)
|
|
else:
|
|
raise e
|
|
|
|
|
|
def get_client(obj):
|
|
return obj.app.client_manager.observabilityclient
|
|
|
|
|
|
def format_labels(d: dict) -> str:
|
|
def replace_doubled_quotes(string):
|
|
if "''" in string:
|
|
string = string.replace("''", "'")
|
|
if '""' in string:
|
|
string = string.replace('""', '"')
|
|
return string
|
|
|
|
ret = ""
|
|
for key, value in d.items():
|
|
ret += "{}='{}', ".format(key, value)
|
|
ret = ret[0:-2]
|
|
old = ""
|
|
while ret != old:
|
|
old = ret
|
|
ret = replace_doubled_quotes(ret)
|
|
return ret
|
|
|
|
|
|
def metrics2cols(m):
|
|
# get all label keys
|
|
cols = list(set().union(*(d.labels.keys() for d in m)))
|
|
cols.sort()
|
|
cols.append("value")
|
|
fields = []
|
|
for metric in m:
|
|
row = [""] * len(cols)
|
|
for key, value in metric.labels.items():
|
|
row[cols.index(key)] = value
|
|
row[cols.index("value")] = metric.value
|
|
fields.append(row)
|
|
return cols, fields
|