From 9fa8e499932f634921ca4fd92349ad791263be56 Mon Sep 17 00:00:00 2001 From: Thomas Carmet <8408330+tcarmet@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:01:23 +0000 Subject: [PATCH] Add branch and event label to metrics --- gh_actions_exporter/config.py | 2 +- gh_actions_exporter/metrics.py | 39 ++++++++++++++++++++++++++++-- gh_actions_exporter/types.py | 10 ++++++++ tests/api/conftest.py | 25 +++++++++++++++++++ tests/api/metrics/conftest.py | 4 ++- tests/api/metrics/test_workflow.py | 38 +++++++++++++++++++++++++++++ 6 files changed, 114 insertions(+), 4 deletions(-) diff --git a/gh_actions_exporter/config.py b/gh_actions_exporter/config.py index 7652ed4..e0f752c 100644 --- a/gh_actions_exporter/config.py +++ b/gh_actions_exporter/config.py @@ -42,7 +42,7 @@ class Settings(BaseSettings): "ubuntu-20.04": 0.008, } default_cost: Optional[float] = 0 - + branches: List[str] = ["main", "master"] check_runs_enabled: bool = False github_app_id: Optional[int] github_app_installation_id: Optional[int] diff --git a/gh_actions_exporter/metrics.py b/gh_actions_exporter/metrics.py index 5324180..2c2117d 100644 --- a/gh_actions_exporter/metrics.py +++ b/gh_actions_exporter/metrics.py @@ -1,10 +1,11 @@ +import fnmatch from typing import Dict, List from prometheus_client import Counter, Histogram from gh_actions_exporter.config import Relabel, RelabelType, Settings from gh_actions_exporter.cost import Cost -from gh_actions_exporter.types import WebHook, WorkflowJob +from gh_actions_exporter.types import WebHook, WorkflowJob, WorkflowRun class Metrics(object): @@ -17,7 +18,10 @@ def __init__(self, settings: Settings): "workflow_name", "repository_visibility", ] - self.workflow_labelnames = self.common_labelnames.copy() + self.workflow_labelnames = self.common_labelnames.copy() + [ + "branch", + "event", + ] self.job_labelnames = self.common_labelnames.copy() + [ "job_name", "runner_type", @@ -107,11 +111,42 @@ def __init__(self, settings: Settings): labelnames=self.job_labelnames, ) + def retrieve_branch(self, workflow_run: WorkflowRun) -> str: + """ + Add the branch label to the metrics exposed by the exporter while + also taking into account the cardinality limitations of prometheus: + + - For workflows triggered on pull_request event + retrieve the base branch (target branch). + + - For other events, retrieve the head branch. + + Then check if the branch matches any of the patterns defined in + self.settings.branches like: development/*, main, feature/*, etc. + + Return the branch name if it matches any of the patterns, + otherwise return "dev". + """ + ref: str + if workflow_run.event == "pull_request": + assert workflow_run.pull_requests + ref = workflow_run.pull_requests[0].base.ref + else: + ref = workflow_run.head_branch + for branch in self.settings.branches: + if fnmatch.fnmatch(ref, branch): + return ref + return "dev" + def workflow_labels(self, webhook: WebHook) -> dict: + assert webhook.workflow_run + branch = self.retrieve_branch(webhook.workflow_run) return dict( workflow_name=webhook.workflow_run.name, repository=webhook.repository.full_name, repository_visibility=webhook.repository.visibility, + branch=branch, + event=webhook.workflow_run.event, ) def runner_type(self, webhook: WebHook) -> str: diff --git a/gh_actions_exporter/types.py b/gh_actions_exporter/types.py index c62d603..9dead64 100644 --- a/gh_actions_exporter/types.py +++ b/gh_actions_exporter/types.py @@ -34,6 +34,15 @@ class WorkflowJob(BaseModel): steps: Optional[list[Steps]] = None +class BasePullRequest(BaseModel): + ref: str + + +class PullRequest(BaseModel): + number: int + base: BasePullRequest + + class WorkflowRun(BaseModel): id: int name: str @@ -44,6 +53,7 @@ class WorkflowRun(BaseModel): workflow_id: int run_number: int head_branch: str + pull_requests: Optional[list[PullRequest]] = None created_at: datetime updated_at: Optional[datetime] = None run_attempt: int diff --git a/tests/api/conftest.py b/tests/api/conftest.py index 8fd39e3..42ec0d2 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -34,6 +34,31 @@ def workflow_run(): "created_at": "2021-11-16T17:52:47Z", "run_started_at": "2021-11-16T17:52:47Z", "updated_at": "2021-11-16T17:53:32Z", + "pull_requests": [ + { + "url": "https://api.github.com/repos/org/repo/pulls/555", + "id": 1805247, + "number": 555, + "head": { + "ref": "feature/my-feature", + "sha": "fc98b69edf5ac3f4789c210e758b1ee4b4396b9e", + "repo": { + "id": 395065315, + "url": "https://api.github.com/repos/org/repo", + "name": "repo", + }, + }, + "base": { + "ref": "master", + "sha": "966917b16d284edde596b0705a49f8c65ecf1c84", + "repo": { + "id": 395065125, + "url": "https://api.github.com/repos/org/repo", + "name": "repo", + }, + }, + } + ], }, "repository": { "name": "repo", diff --git a/tests/api/metrics/conftest.py b/tests/api/metrics/conftest.py index 0361d22..ef35391 100644 --- a/tests/api/metrics/conftest.py +++ b/tests/api/metrics/conftest.py @@ -27,7 +27,9 @@ def job_relabel_config(): @lru_cache() def default_settings(): - return Settings() + return Settings( + branches=["main", "master", "development/*"], + ) @lru_cache() diff --git a/tests/api/metrics/test_workflow.py b/tests/api/metrics/test_workflow.py index 04a30e8..8e18a07 100644 --- a/tests/api/metrics/test_workflow.py +++ b/tests/api/metrics/test_workflow.py @@ -54,3 +54,41 @@ def test_rebuild(client, workflow_run, headers): for line in metrics.text.split("\n"): if "workflow_rebuild_count_total{" in line: assert "1.0" in line + + +def test_branch_label(client, workflow_run, headers): + # Ensure a feature branch is not indexed + upload = client.post("/webhook", json=workflow_run, headers=headers) + assert upload.status_code == 202 + + metrics = client.get("/metrics") + assert metrics.status_code == 200 + assert 'branch="feature"' not in metrics.text + assert 'branch="dev"' in metrics.text + + # Index a push event with a branch label + workflow_run["workflow_run"]["head_branch"] = "main" + upload = client.post("/webhook", json=workflow_run, headers=headers) + assert upload.status_code == 202 + + metrics = client.get("/metrics") + assert metrics.status_code == 200 + assert 'branch="main"' in metrics.text + + # Index a branch with a fnmatch pattern configured + workflow_run["workflow_run"]["head_branch"] = "development/1.2" + upload = client.post("/webhook", json=workflow_run, headers=headers) + assert upload.status_code == 202 + + metrics = client.get("/metrics") + assert metrics.status_code == 200 + assert 'branch="development/1.2"' in metrics.text + + # Index the base branch of a pull request + workflow_run["workflow_run"]["event"] = "pull_request" + upload = client.post("/webhook", json=workflow_run, headers=headers) + assert upload.status_code == 202 + + metrics = client.get("/metrics") + assert metrics.status_code == 200 + assert 'branch="master"' in metrics.text