|
1 | 1 | import dataclasses |
2 | 2 | from dataclasses import dataclass |
3 | | -from datetime import datetime, timezone |
| 3 | +from datetime import datetime, timedelta, timezone |
| 4 | +from typing import Collection |
4 | 5 |
|
5 | | -from sqlalchemy import insert |
| 6 | +from sqlalchemy import insert, select |
6 | 7 |
|
7 | 8 | from athenian.api.db import DatabaseLike |
8 | 9 | from athenian.api.internal.features.entries import MinePullRequestMetrics |
|
11 | 12 | from athenian.api.internal.miners.github.release_load import MineReleaseMetrics |
12 | 13 | from athenian.api.internal.reposet import RepositorySetMetrics |
13 | 14 | from athenian.api.models.persistentdata.models import HealthMetric |
| 15 | +from athenian.api.models.web import AccountHealth |
14 | 16 |
|
15 | 17 |
|
16 | 18 | @dataclass(frozen=True, slots=True) |
@@ -47,3 +49,110 @@ async def persist(self, account: int, rdb: DatabaseLike) -> None: |
47 | 49 | m.created_at = now |
48 | 50 | values.append(m.explode(with_primary_keys=True)) |
49 | 51 | await rdb.execute_many(insert(HealthMetric), values) |
| 52 | + |
| 53 | + |
| 54 | +async def measure_accounts_health( |
| 55 | + ids: Collection[int], |
| 56 | + time_points: list[datetime], |
| 57 | + rdb: DatabaseLike, |
| 58 | +): |
| 59 | + """Collect all the supported health metrics for each account in `ids`.""" |
| 60 | + rows = await rdb.fetch_all( |
| 61 | + select(HealthMetric) |
| 62 | + .where( |
| 63 | + HealthMetric.account_id.in_(ids), |
| 64 | + HealthMetric.created_at >= time_points[0], |
| 65 | + HealthMetric.created_at < time_points[-1] + timedelta(hours=1), |
| 66 | + ) |
| 67 | + .order_by(HealthMetric.created_at), |
| 68 | + ) |
| 69 | + ltp = len(time_points) |
| 70 | + result = { |
| 71 | + acc: AccountHealth( |
| 72 | + broken_branches=[0] * ltp, |
| 73 | + broken_dags=[0] * ltp, |
| 74 | + deployments=[0] * ltp, |
| 75 | + empty_releases=[0] * ltp, |
| 76 | + endpoint_p50={}, |
| 77 | + endpoint_p95={}, |
| 78 | + event_releases=[0] * ltp, |
| 79 | + inconsistent_nodes={}, |
| 80 | + pending_fetch_branches=[0] * ltp, |
| 81 | + pending_fetch_prs=[0] * ltp, |
| 82 | + prs_count=[0] * ltp, |
| 83 | + released_prs_ratio=[0] * ltp, |
| 84 | + reposet_problems=[0] * ltp, |
| 85 | + repositories_count=[0] * ltp, |
| 86 | + unresolved_deployments=[0] * ltp, |
| 87 | + unresolved_releases=[0] * ltp, |
| 88 | + ) |
| 89 | + for acc in ids |
| 90 | + } |
| 91 | + start_time = time_points[0] |
| 92 | + one_hour = timedelta(hours=1) |
| 93 | + acc_col = HealthMetric.account_id.name |
| 94 | + created_col = HealthMetric.created_at.name |
| 95 | + name_col = HealthMetric.name.name |
| 96 | + value_col = HealthMetric.value.name |
| 97 | + for row in rows: |
| 98 | + model = result[row[acc_col]] |
| 99 | + dt = row[created_col] |
| 100 | + pos = (dt - start_time) // one_hour |
| 101 | + val = row[value_col] |
| 102 | + match row[name_col]: |
| 103 | + case "branches_count": |
| 104 | + continue |
| 105 | + case ["branches_empty_count", "branches_no_default"]: |
| 106 | + model.broken_branches[pos] += val |
| 107 | + case ["commits_pristine", "commits_corrupted", "commits_orphaned"]: |
| 108 | + model.broken_dags[pos] += val |
| 109 | + case "deployments_count": |
| 110 | + model.deployments[pos] = val |
| 111 | + case "deployments_unresolved": |
| 112 | + model.unresolved_deployments[pos] = val |
| 113 | + case "releases_unresolved": |
| 114 | + model.unresolved_releases[pos] = val |
| 115 | + case "releases_by_event": |
| 116 | + model.event_releases[pos] = val |
| 117 | + case "releases_empty": |
| 118 | + model.empty_releases[pos] = val |
| 119 | + case "reposet_problems": |
| 120 | + model.empty_releases[pos] = val |
| 121 | + case "reposet_length": |
| 122 | + model.repositories_count[pos] = val |
| 123 | + case "prs_count": |
| 124 | + model.prs_count[pos] = val |
| 125 | + case "prs_done_count": |
| 126 | + model.released_prs_ratio[pos] = val |
| 127 | + case p50 if p50.startswith("p50/"): |
| 128 | + endpoint = p50[4:] |
| 129 | + try: |
| 130 | + model.endpoint_p50[endpoint][pos] = val |
| 131 | + except KeyError: |
| 132 | + vals = [None] * ltp |
| 133 | + vals[pos] = val |
| 134 | + model.endpoint_p50[endpoint] = vals |
| 135 | + case p95 if p95.startswith("p95/"): |
| 136 | + endpoint = p95[4:] |
| 137 | + try: |
| 138 | + model.endpoint_p95[endpoint][pos] = val |
| 139 | + except KeyError: |
| 140 | + vals = [None] * ltp |
| 141 | + vals[pos] = val |
| 142 | + model.endpoint_p95[endpoint] = vals |
| 143 | + case node if node.startswith("inconsistency/"): |
| 144 | + node = node[14:] |
| 145 | + try: |
| 146 | + model.inconsistent_nodes[node][pos] = val |
| 147 | + except KeyError: |
| 148 | + vals = [0] * ltp |
| 149 | + vals[pos] = val |
| 150 | + model.inconsistent_nodes[node] = vals |
| 151 | + for model in result.values(): |
| 152 | + released_prs_ratio = model.released_prs_ratio |
| 153 | + for i, (done, count) in enumerate(zip(released_prs_ratio, model.prs_count)): |
| 154 | + if count == 0: |
| 155 | + released_prs_ratio[i] = 0 |
| 156 | + else: |
| 157 | + released_prs_ratio[i] = done / count |
| 158 | + return result |
0 commit comments