← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~ack/maas-kpi:aggregate-stats into maas-kpi:master

 

Alberto Donato has proposed merging ~ack/maas-kpi:aggregate-stats into maas-kpi:master.

Commit message:
rework daily stat script to use dataclasses for stats


Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~ack/maas-kpi/+git/maas-kpi/+merge/442831
-- 
Your team MAAS Committers is requested to review the proposed merge of ~ack/maas-kpi:aggregate-stats into maas-kpi:master.
diff --git a/maaskpi/dailystats.py b/maaskpi/dailystats.py
index e2bdab2..e034731 100644
--- a/maaskpi/dailystats.py
+++ b/maaskpi/dailystats.py
@@ -4,7 +4,7 @@ import gzip
 import json
 import re
 from collections import defaultdict
-from dataclasses import dataclass, field
+from dataclasses import asdict, dataclass, field, fields
 from datetime import date, datetime, timedelta
 from pathlib import Path
 from urllib.parse import parse_qs, urlparse
@@ -130,41 +130,46 @@ class DailyRequestsSeries(SeriesHelper):
         autocommit = False
 
 
+@dataclass
+class DeploymentStats:
+    """Stats for a deployment."""
+
+    machines: int = 0
+    devices: int = 0
+    regionrack_controllers: int = 0
+    region_controllers: int = 0
+    rack_controllers: int = 0
+    workload_annotations_machines: int = 0
+    workload_annotations_total: int = 0
+    workload_annotations_unique_keys: int = 0
+    workload_annotations_unique_values: int = 0
+    # These might make sense as a different series, with lxd/virsh being
+    # encoded as a tag. On other hand, it's not something we want to filter on
+    # in Grafana.
+    vm_hosts_lxd_total: int = 0
+    vm_hosts_virsh_total: int = 0
+    vm_hosts_lxd_vms: int = 0
+    vm_hosts_virsh_vms: int = 0
+    vm_hosts_lxd_available_cores: int = 0
+    vm_hosts_virsh_available_cores: int = 0
+    vm_hosts_lxd_available_over_cores: int = 0
+    vm_hosts_virsh_available_over_cores: int = 0
+    vm_hosts_lxd_utilized_cores: int = 0
+    vm_hosts_virsh_utilized_cores: int = 0
+    vm_hosts_lxd_available_memory: int = 0
+    vm_hosts_virsh_available_memory: int = 0
+    vm_hosts_lxd_available_over_memory: int = 0
+    vm_hosts_virsh_available_over_memory: int = 0
+    vm_hosts_lxd_utilized_memory: int = 0
+    vm_hosts_virsh_utilized_memory: int = 0
+    subnets_v4: int = 0
+    subnets_v6: int = 0
+
+
 class DailyStatsSeries(SeriesHelper):
     class Meta:
         series_name = "maas.daily_stats"
-        fields = [
-            "machines",
-            "devices",
-            "regionrack_controllers",
-            "region_controllers",
-            "rack_controllers",
-            "workload_annotations_machines",
-            "workload_annotations_total",
-            "workload_annotations_unique_keys",
-            "workload_annotations_unique_values",
-            # These might make sense as a different series, with
-            # lxd/virsh being encoded as a tag. On other hand, it's not
-            # something we want to filter on in Grafana.
-            "vm_hosts_lxd_total",
-            "vm_hosts_virsh_total",
-            "vm_hosts_lxd_vms",
-            "vm_hosts_virsh_vms",
-            "vm_hosts_lxd_available_cores",
-            "vm_hosts_virsh_available_cores",
-            "vm_hosts_lxd_available_over_cores",
-            "vm_hosts_virsh_available_over_cores",
-            "vm_hosts_lxd_utilized_cores",
-            "vm_hosts_virsh_utilized_cores",
-            "vm_hosts_lxd_available_memory",
-            "vm_hosts_virsh_available_memory",
-            "vm_hosts_lxd_available_over_memory",
-            "vm_hosts_virsh_available_over_memory",
-            "vm_hosts_lxd_utilized_memory",
-            "vm_hosts_virsh_utilized_memory",
-            "subnets_v4",
-            "subnets_v6",
-        ]
+        fields = [field.name for field in fields(DeploymentStats)]
         tags = ["maas_version", "uuid"]
         autocommit = False
 
@@ -185,68 +190,127 @@ def get_date(name):
     return datetime.strptime(date_string, "%Y%m%d").date()
 
 
-def normalize_data_dict(normalized):
-    """Normalize the data dict to make it easier to work with.
+def get_entry_stats(data) -> DeploymentStats:
+    """Return stats for an entry."""
 
-    Older versions of MAAS might not have all the data, or have keys
-    with different names. This function adds the missing keys and
-    renames the old keys, so that the callsites can work with the latest
-    version of the data dict schema.
-    """
-    if "workload_annotations" not in normalized:
-        normalized["workload_annotations"] = {
-            "annotated_machines": 0,
-            "total_annotations": 0,
-            "unique_keys": 0,
-            "unique_values": 0,
-        }
-    if "vm_hosts" not in normalized:
-        normalized["vm_hosts"] = {}
-        for vm_type in ["lxd", "virsh"]:
-            normalized["vm_hosts"][vm_type] = {
-                "vm_hosts": 0,
-                "vms": 0,
-                "available_resources": {
-                    "cores": 0,
-                    "memory": 0,
-                    "local_storage": 0,
-                    "over_cores": 0,
-                    "over_memory": 0,
-                },
-                "utilized_resources": {
-                    "cores": 0,
-                    "memory": 0,
-                    "storage": 0,
-                },
-            }
-    if "network_stats" not in normalized:
-        normalized["network_stats"] = {
-            "spaces": 0,
-            "fabrics": 0,
-            "vlans": 0,
-            "subnets_v4": 0,
-            "subnets_v6": 0,
-        }
-    if "bmcs" not in normalized:
-        normalized["bmcs"] = {
-            "auto_detected": {},
-            "user_created": {},
-            "unknown": {},
-        }
-    normalized["vm_hosts"]["lxd"]["available_resources"]["over_cores"] = int(
-        normalized["vm_hosts"]["lxd"]["available_resources"]["over_cores"]
-    )
-    normalized["vm_hosts"]["virsh"]["available_resources"]["over_cores"] = int(
-        normalized["vm_hosts"]["virsh"]["available_resources"]["over_cores"]
-    )
-    normalized["vm_hosts"]["lxd"]["available_resources"]["over_memory"] = int(
-        normalized["vm_hosts"]["lxd"]["available_resources"]["over_memory"]
-    )
-    normalized["vm_hosts"]["virsh"]["available_resources"]["over_memory"] = int(
-        normalized["vm_hosts"]["virsh"]["available_resources"]["over_memory"]
+    def get(*keys: str) -> int:
+        stats = data
+        for key in keys:
+            try:
+                stats = stats[key]
+            except (KeyError, TypeError):
+                # either the key is not found, or data is not a dict
+                return 0
+        try:
+            return int(stats)
+        except TypeError:
+            return 0
+
+    return DeploymentStats(
+        machines=get("nodes", "machines"),
+        devices=get("nodes", "devices"),
+        regionrack_controllers=get("controllers", "regionracks"),
+        region_controllers=get("controllers", "regions"),
+        rack_controllers=get("controllers", "racks"),
+        vm_hosts_lxd_total=get("vm_hosts", "lxd", "vm_hosts"),
+        vm_hosts_virsh_total=get("vm_hosts", "virsh", "vm_hosts"),
+        vm_hosts_lxd_vms=get("vm_hosts", "lxd", "vms"),
+        vm_hosts_virsh_vms=get("vm_hosts", "virsh", "vms"),
+        vm_hosts_lxd_available_cores=get(
+            "vm_hosts", "lxd", "available_resources", "cores"
+        ),
+        vm_hosts_virsh_available_cores=get(
+            "vm_hosts",
+            "virsh",
+            "available_resources",
+            "cores",
+        ),
+        vm_hosts_lxd_available_over_cores=get(
+            "vm_hosts",
+            "lxd",
+            "available_resources",
+            "over_cores",
+        ),
+        vm_hosts_virsh_available_over_cores=get(
+            "vm_hosts",
+            "virsh",
+            "available_resources",
+            "over_cores",
+        ),
+        vm_hosts_lxd_utilized_cores=get(
+            "vm_hosts",
+            "lxd",
+            "utilized_resources",
+            "cores",
+        ),
+        vm_hosts_virsh_utilized_cores=get(
+            "vm_hosts", "virsh", "utilized_resources", "cores"
+        ),
+        vm_hosts_lxd_available_memory=get(
+            "vm_hosts",
+            "lxd",
+            "available_resources",
+            "memory",
+        ),
+        vm_hosts_virsh_available_memory=get(
+            "vm_hosts",
+            "virsh",
+            "available_resources",
+            "memory",
+        ),
+        vm_hosts_lxd_available_over_memory=get(
+            "vm_hosts",
+            "lxd",
+            "available_resources",
+            "over_memory",
+        ),
+        vm_hosts_virsh_available_over_memory=get(
+            "vm_hosts",
+            "virsh",
+            "available_resources",
+            "over_memory",
+        ),
+        vm_hosts_lxd_utilized_memory=get(
+            "vm_hosts",
+            "lxd",
+            "utilized_resources",
+            "memory",
+        ),
+        vm_hosts_virsh_utilized_memory=get(
+            "vm_hosts",
+            "virsh",
+            "utilized_resources",
+            "memory",
+        ),
+        workload_annotations_machines=get(
+            "workload_annotations",
+            "annotated_machines",
+        ),
+        workload_annotations_total=get(
+            "workload_annotations",
+            "total_annotations",
+        ),
+        workload_annotations_unique_keys=get(
+            "workload_annotations",
+            "unique_keys",
+        ),
+        workload_annotations_unique_values=get(
+            "workload_annotations",
+            "unique_values",
+        ),
+        subnets_v4=get("network_stats", "subnets_v4"),
+        subnets_v6=get("network_stats", "subnets_v6"),
     )
 
-    return normalized
+
+def get_bmc_stats(data):
+    """Return and normalize bmc stats."""
+    default = {
+        "auto_detected": {},
+        "user_created": {},
+        "unknown": {},
+    }
+    return data.get("bmcs", default)
 
 
 class DailyStats:
@@ -287,10 +351,14 @@ class DailyStats:
                 image_arch = parts[5]
             self.image_entries[item.uuid].add((item.version, image_series, image_arch))
         query = parse_qs(urlparse(item.path).query)
-        if "data" in query:
-            self.with_data += 1
-            raw_data = base64.b64decode(query["data"][0]).decode()
-            self.entries[item.uuid]["data"] = json.loads(json.loads(raw_data))
+        if "data" not in query:
+            return
+
+        self.with_data += 1
+        raw_data = base64.b64decode(query["data"][0]).decode()
+        data = json.loads(json.loads(raw_data))
+        self.entries[item.uuid]["stats"] = get_entry_stats(data)
+        self.entries[item.uuid]["bmc_stats"] = get_bmc_stats(data)
 
     def create_series(self):
         """Create DailyStatsSeries measurements based on the internal data."""
@@ -299,92 +367,31 @@ class DailyStats:
             f"without uuids, {self.with_data} with data"
         )
         timestamp = get_nanosecond_timestamp(self.day)
-        for uuid, info in sorted(self.entries.items()):
+        for uuid, info in self.entries.items():
             DailyRequestsSeries(
                 time=timestamp,
                 maas_version=info["version"],
                 uuid=uuid,
                 field_uuid=uuid,
             )
-
-            if "data" not in info:
-                continue
-            data = normalize_data_dict(dict(info.pop("data")))
-            workload_annotations = data["workload_annotations"]
-            nodes = data["nodes"]
-            controllers = data["controllers"]
-            vm_hosts = data["vm_hosts"]
-            network = data["network_stats"]
-            for creation_method, bmcs in data["bmcs"].items():
-                for driver_name, count in bmcs.items():
-                    DailyPowerDriverSeries(
-                        time=timestamp,
-                        maas_version=info["version"],
-                        uuid=uuid,
-                        count=count,
-                        power_driver_name=driver_name,
-                        power_driver_creation=creation_method,
-                    )
-            DailyStatsSeries(
-                time=timestamp,
-                maas_version=info["version"],
-                uuid=uuid,
-                machines=nodes["machines"],
-                devices=nodes["devices"],
-                regionrack_controllers=controllers["regionracks"],
-                region_controllers=controllers["regions"],
-                rack_controllers=controllers["racks"],
-                vm_hosts_lxd_total=vm_hosts["lxd"]["vm_hosts"],
-                vm_hosts_virsh_total=vm_hosts["virsh"]["vm_hosts"],
-                vm_hosts_lxd_vms=vm_hosts["lxd"]["vms"],
-                vm_hosts_virsh_vms=vm_hosts["virsh"]["vms"],
-                vm_hosts_lxd_available_cores=vm_hosts["lxd"]["available_resources"][
-                    "cores"
-                ],
-                vm_hosts_virsh_available_cores=vm_hosts["virsh"]["available_resources"][
-                    "cores"
-                ],
-                vm_hosts_lxd_available_over_cores=vm_hosts["lxd"][
-                    "available_resources"
-                ]["over_cores"],
-                vm_hosts_virsh_available_over_cores=vm_hosts["virsh"][
-                    "available_resources"
-                ]["over_cores"],
-                vm_hosts_lxd_utilized_cores=vm_hosts["lxd"]["utilized_resources"][
-                    "cores"
-                ],
-                vm_hosts_virsh_utilized_cores=vm_hosts["virsh"]["utilized_resources"][
-                    "cores"
-                ],
-                vm_hosts_lxd_available_memory=vm_hosts["lxd"]["available_resources"][
-                    "memory"
-                ],
-                vm_hosts_virsh_available_memory=vm_hosts["virsh"][
-                    "available_resources"
-                ]["memory"],
-                vm_hosts_lxd_available_over_memory=vm_hosts["lxd"][
-                    "available_resources"
-                ]["over_memory"],
-                vm_hosts_virsh_available_over_memory=vm_hosts["virsh"][
-                    "available_resources"
-                ]["over_memory"],
-                vm_hosts_lxd_utilized_memory=vm_hosts["lxd"]["utilized_resources"][
-                    "memory"
-                ],
-                vm_hosts_virsh_utilized_memory=vm_hosts["virsh"]["utilized_resources"][
-                    "memory"
-                ],
-                workload_annotations_machines=workload_annotations[
-                    "annotated_machines"
-                ],
-                workload_annotations_total=workload_annotations["total_annotations"],
-                workload_annotations_unique_keys=workload_annotations["unique_keys"],
-                workload_annotations_unique_values=workload_annotations[
-                    "unique_values"
-                ],
-                subnets_v4=network["subnets_v4"],
-                subnets_v6=network["subnets_v6"],
-            )
+            if bmc_stats := info.get("bmc_stats"):
+                for creation_method, bmcs in bmc_stats.items():
+                    for driver_name, count in bmcs.items():
+                        DailyPowerDriverSeries(
+                            time=timestamp,
+                            maas_version=info["version"],
+                            uuid=uuid,
+                            count=count,
+                            power_driver_name=driver_name,
+                            power_driver_creation=creation_method,
+                        )
+            if stats := info.get("stats"):
+                DailyStatsSeries(
+                    time=timestamp,
+                    maas_version=info["version"],
+                    uuid=uuid,
+                    **asdict(stats),
+                )
 
         for uuid, image_requests in sorted(self.image_entries.items()):
             for maas_version, series, architecture in image_requests:
@@ -583,16 +590,16 @@ class DailyStatsCollector(Collector):
         log_files = LogFiles(swift, days, cache_dir)
         log_files.init()
         if not log_files.hosts:
-            print("No hosts!")
+            print("No logs hosts found!")
         for host_name, host in log_files.hosts.items():
-            print(f"{host_name}: {len(host.file_names)}")
+            print(f"Collected {len(host.file_names)} logs files from {host_name}")
 
         # Ignore the first day, since it's most likely incomplete.
         log_files.get_next_day()
         day_stats = None
         while True:
             items = log_files.get_next_day()
-            print(f"{log_files.current_day}: {len(items)}")
+            print(f"Stats for {log_files.current_day}: {len(items)}")
             if not items:
                 break
             if day_stats is not None:

Follow ups