|
18 | 18 |
|
19 | 19 | import uuid |
20 | 20 |
|
21 | | -import requests |
| 21 | +from prometheus_api_client import PrometheusConnect |
22 | 22 | from oslo_config import cfg |
23 | 23 | from oslo_log import log |
24 | 24 |
|
|
34 | 34 | help="Prometheus server endpoint URL.", |
35 | 35 | ), |
36 | 36 | cfg.StrOpt( |
37 | | - "prometheus_query", |
38 | | - default="sum(rate(libvirt_domain_info_energy_consumption_joules_total" |
39 | | - '{uuid=~"{{uuid}}"}[5m])) * 300 / 3600', |
40 | | - help="Prometheus query to retrieve energy consumption in Wh. " |
41 | | - "The query can use {{uuid}} as a template variable for the VM UUID.", |
| 37 | + "prometheus_metric_name", |
| 38 | + default="prometheus_value", |
| 39 | + help="Name of the Prometheus metric to query for energy consumption.", |
| 40 | + ), |
| 41 | + cfg.StrOpt( |
| 42 | + "prometheus_label_type_instance", |
| 43 | + default="scaph_process_power_microwatts", |
| 44 | + help="Value for the type_instance label in Prometheus queries.", |
42 | 45 | ), |
43 | 46 | cfg.IntOpt( |
44 | | - "prometheus_timeout", |
| 47 | + "prometheus_step_seconds", |
45 | 48 | default=30, |
46 | | - help="Timeout for Prometheus API requests in seconds.", |
| 49 | + help="Frequency between samples in the time series (in seconds).", |
| 50 | + ), |
| 51 | + cfg.StrOpt( |
| 52 | + "prometheus_query_range", |
| 53 | + default="1h", |
| 54 | + help="Query time range (e.g., '1h', '6h', '24h').", |
| 55 | + ), |
| 56 | + cfg.BoolOpt( |
| 57 | + "prometheus_verify_ssl", |
| 58 | + default=True, |
| 59 | + help="Whether to verify SSL when connecting to Prometheus.", |
47 | 60 | ), |
48 | 61 | ] |
49 | 62 |
|
@@ -72,39 +85,61 @@ def _get_flavors(self): |
72 | 85 | LOG.warning(f"Could not get flavors: {e}") |
73 | 86 | return flavors |
74 | 87 |
|
75 | | - def _query_prometheus(self, query, timestamp=None): |
76 | | - """Query Prometheus API and return results. |
| 88 | + def _energy_consumed_wh(self, vm_uuid): |
| 89 | + """Calculate the energy consumed (Wh) for a VM from Prometheus. |
| 90 | +
|
| 91 | + This function queries Prometheus for instantaneous power samples |
| 92 | + (in microwatts) and calculates the energy consumed in Watt-hours. |
77 | 93 |
|
78 | | - :param query: PromQL query string |
79 | | - :param timestamp: Optional timestamp for query (datetime object) |
80 | | - :returns: Query results |
| 94 | + :param vm_uuid: UUID of the VM to query energy for |
| 95 | + :returns: Energy consumed in Watt-hours (Wh) |
81 | 96 | """ |
82 | | - endpoint = CONF.prometheus.prometheus_endpoint |
83 | | - url = f"{endpoint}/api/v1/query" |
| 97 | + prom_url = CONF.prometheus.prometheus_endpoint |
| 98 | + metric_name = CONF.prometheus.prometheus_metric_name |
| 99 | + step_seconds = CONF.prometheus.prometheus_step_seconds |
| 100 | + query_range = CONF.prometheus.prometheus_query_range |
| 101 | + verify_ssl = CONF.prometheus.prometheus_verify_ssl |
| 102 | + |
| 103 | + prom = PrometheusConnect(url=prom_url, disable_ssl=not verify_ssl) |
| 104 | + |
| 105 | + # factor = step_seconds / 3600 converts µW·s to µWh |
| 106 | + factor = step_seconds / 3600 |
| 107 | + |
| 108 | + # Build labels for this VM |
| 109 | + labels = { |
| 110 | + "type_instance": CONF.prometheus.prometheus_label_type_instance, |
| 111 | + "uuid": vm_uuid, |
| 112 | + } |
| 113 | + |
| 114 | + # Build label string: {key="value", ...} |
| 115 | + label_selector = ",".join(f'{k}="{v}"' for k, v in labels.items()) |
| 116 | + |
| 117 | + # Construct the PromQL query |
| 118 | + query = ( |
| 119 | + f"sum_over_time({metric_name}{{{label_selector}}}[{query_range}]) " |
| 120 | + f"* {factor} / 1000000" |
| 121 | + ) |
84 | 122 |
|
85 | | - params = {"query": query} |
86 | | - if timestamp: |
87 | | - params["time"] = int(timestamp.timestamp()) |
| 123 | + LOG.debug(f"Querying Prometheus for VM {vm_uuid} with query: {query}") |
88 | 124 |
|
89 | 125 | try: |
90 | | - response = requests.get( |
91 | | - url, params=params, timeout=CONF.prometheus.prometheus_timeout |
92 | | - ) |
93 | | - response.raise_for_status() |
94 | | - data = response.json() |
95 | | - |
96 | | - if data.get("status") != "success": |
97 | | - error_msg = data.get("error", "Unknown error") |
98 | | - LOG.error(f"Prometheus query failed: {error_msg}") |
99 | | - return None |
100 | | - |
101 | | - return data.get("data", {}).get("result", []) |
102 | | - except requests.exceptions.RequestException as e: |
103 | | - LOG.error(f"Failed to query Prometheus: {e}") |
104 | | - return None |
| 126 | + # Run query |
| 127 | + result = prom.custom_query(query=query) |
| 128 | + |
| 129 | + if not result: |
| 130 | + LOG.debug(f"No energy data returned for VM {vm_uuid}") |
| 131 | + return 0.0 |
| 132 | + |
| 133 | + energy_wh = float(result[0]["value"][1]) |
| 134 | + LOG.debug(f"VM {vm_uuid} consumed {energy_wh:.4f} Wh") |
| 135 | + return energy_wh |
| 136 | + |
| 137 | + except (KeyError, IndexError, ValueError) as e: |
| 138 | + LOG.warning(f"Error parsing Prometheus result for VM {vm_uuid}: {e}") |
| 139 | + return 0.0 |
105 | 140 | except Exception as e: |
106 | | - LOG.error(f"Unexpected error querying Prometheus: {e}") |
107 | | - return None |
| 141 | + LOG.error(f"Error querying Prometheus for VM {vm_uuid}: {e}") |
| 142 | + return 0.0 |
108 | 143 |
|
109 | 144 | def _get_servers(self, extract_from): |
110 | 145 | """Get all servers for a given date.""" |
@@ -239,48 +274,31 @@ def extract(self, extract_from, extract_to): |
239 | 274 | ) |
240 | 275 |
|
241 | 276 | # Query Prometheus for each server |
242 | | - query_template = CONF.prometheus.prometheus_query |
243 | | - |
244 | 277 | for server in servers: |
245 | 278 | vm_uuid = str(server.id) |
246 | 279 | vm_name = server.name |
247 | 280 |
|
248 | | - # Replace template variables in the query |
249 | | - query = query_template.replace("{{uuid}}", vm_uuid) |
| 281 | + LOG.debug(f"Querying energy consumption for VM {vm_name} ({vm_uuid})") |
250 | 282 |
|
251 | | - LOG.debug( |
252 | | - f"Querying Prometheus for VM {vm_name} ({vm_uuid}) " |
253 | | - f"with query: {query}" |
254 | | - ) |
255 | | - |
256 | | - results = self._query_prometheus(query, extract_to) |
| 283 | + # Get energy consumption using the new method |
| 284 | + energy_value = self._energy_consumed_wh(vm_uuid) |
257 | 285 |
|
258 | | - if results is None: |
259 | | - LOG.warning( |
260 | | - f"No results returned from Prometheus for VM " |
261 | | - f"{vm_name} ({vm_uuid})" |
| 286 | + if energy_value <= 0: |
| 287 | + LOG.debug( |
| 288 | + f"No energy consumption data for VM {vm_name} ({vm_uuid}), " |
| 289 | + "skipping record creation" |
262 | 290 | ) |
263 | 291 | continue |
264 | 292 |
|
265 | | - # Process results and create records |
266 | | - for result in results: |
267 | | - value = result.get("value", []) |
268 | | - |
269 | | - if len(value) < 2: |
270 | | - continue |
271 | | - |
272 | | - # value is [timestamp, value_string] |
273 | | - energy_value = float(value[1]) |
274 | | - |
275 | | - LOG.debug( |
276 | | - f"Creating energy record: {energy_value} Wh " |
277 | | - f"for VM {vm_name} ({vm_uuid})" |
278 | | - ) |
| 293 | + LOG.debug( |
| 294 | + f"Creating energy record: {energy_value} Wh " |
| 295 | + f"for VM {vm_name} ({vm_uuid})" |
| 296 | + ) |
279 | 297 |
|
280 | | - energy_record = self._build_energy_record( |
281 | | - server, energy_value, extract_from, extract_to |
282 | | - ) |
283 | | - records.append(energy_record) |
| 298 | + energy_record = self._build_energy_record( |
| 299 | + server, energy_value, extract_from, extract_to |
| 300 | + ) |
| 301 | + records.append(energy_record) |
284 | 302 |
|
285 | 303 | LOG.info(f"Extracted {len(records)} energy records for project {self.project}") |
286 | 304 |
|
|
0 commit comments