Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
3c84f04
add prometheus client to dependency & order deps alphabetically
yanksyoon Jun 23, 2025
5188ef2
add metrics route
yanksyoon Jun 23, 2025
98cd349
refactor internal function
yanksyoon Jun 23, 2025
1766519
add reconcile metrics
yanksyoon Jun 23, 2025
c2b7fc3
implement reconcile metrics
yanksyoon Jun 23, 2025
d55b890
initial implementation
yanksyoon Jun 23, 2025
cb74f97
test: add integration test
yanksyoon Jun 23, 2025
a22a736
test: test flavor label
yanksyoon Jun 23, 2025
93aeb5e
fix: enable allure test to collect reports before running tests
yanksyoon Jun 24, 2025
949ec41
remove dep
yanksyoon Jun 24, 2025
9649c53
debug: try two tests
yanksyoon Jun 24, 2025
bc76e06
test: mark openstack test
yanksyoon Jun 24, 2025
cda9318
test: move integration test pre run script
yanksyoon Jun 24, 2025
ea5dc14
fix: juju microk8s setup script
yanksyoon Jun 24, 2025
744769c
test: dont switch back to original controller
yanksyoon Jun 24, 2025
83b5ccc
switch back to original controller
yanksyoon Jun 24, 2025
9c3fd26
unset juju model env var
yanksyoon Jun 24, 2025
b99962a
raw json output
yanksyoon Jun 24, 2025
dec85d2
debug
yanksyoon Jun 25, 2025
4cbf8d1
Merge branch 'main' of https://github.com/canonical/github-runner-ope…
yanksyoon Jun 25, 2025
d757a98
fix: label name
yanksyoon Jun 25, 2025
edde27e
test: fix offer name relation
yanksyoon Jun 25, 2025
ff43e8a
test: do not use juju controller from env var
yanksyoon Jun 25, 2025
a4debe2
test: do not raise on error while waiting for idle
yanksyoon Jun 25, 2025
3bf6c5a
test: model integrate & consume
yanksyoon Jun 25, 2025
4d65ffc
test: keep microk8s model
yanksyoon Jun 25, 2025
eb13f7a
test: deploy cos agent
yanksyoon Jun 25, 2025
61cad41
test: add grafana agent series
yanksyoon Jun 25, 2025
f28f814
test: fix typo
yanksyoon Jun 25, 2025
9a4b956
test: assign grafana agent channel
yanksyoon Jun 25, 2025
0fb23f0
test: juju ops lib is ...
yanksyoon Jun 25, 2025
61ceb1f
test: use series
yanksyoon Jun 25, 2025
e2a558b
test: swap out ops_test with jubilant
yanksyoon Jun 26, 2025
580aa68
test: generator type hint fix
yanksyoon Jun 26, 2025
09fc69a
test: add prometheus datasource
yanksyoon Jun 26, 2025
717af98
test: separate micrlk8s juju from lxd juju
yanksyoon Jun 26, 2025
5639311
test: add controller prefix to model
yanksyoon Jun 26, 2025
62ee73d
test: store model & controller name
yanksyoon Jun 26, 2025
d187de6
test: remove duplicate model naming w/ controller name prefix
yanksyoon Jun 26, 2025
ea2cb4f
test: add fixture dependency
yanksyoon Jun 26, 2025
2b021a7
test: switch controller fixture for juju offers
yanksyoon Jun 26, 2025
c85ac08
test: switch controller select model
yanksyoon Jun 26, 2025
2788589
test: modify juju controller & model env vars
yanksyoon Jun 26, 2025
abd07a4
test: try direct cli_bin call
yanksyoon Jun 26, 2025
fe67e92
test: try no env patch
yanksyoon Jun 26, 2025
9a852af
test: assert subprocess result
yanksyoon Jun 26, 2025
5b5a7d5
test: set additional juju envs
yanksyoon Jun 26, 2025
933b9f1
test: use controller & model params
yanksyoon Jun 27, 2025
14cf50e
test: try consume model w/ model params
yanksyoon Jun 27, 2025
37d2bb6
test: use direct consume command
yanksyoon Jun 27, 2025
9a8f67c
test: setup microstack metallb for ingress
yanksyoon Jun 27, 2025
649597a
test: check for metrics availability
yanksyoon Jun 27, 2025
e32efb0
test: add schema to request
yanksyoon Jun 27, 2025
62cf23f
test: fix syntax err
yanksyoon Jun 27, 2025
09f4042
test: fix prometheus ip
yanksyoon Jun 27, 2025
1f71e5e
test: wait for openstack metrics longer
yanksyoon Jun 27, 2025
8546817
test: test only metrics that have been generated
yanksyoon Jun 29, 2025
6271b74
test: restore tests
yanksyoon Jun 29, 2025
28e7e99
chore: move integration test script
yanksyoon Jun 29, 2025
c5e96ec
ci: separate tests depending on k8s
yanksyoon Jun 29, 2025
80edf02
test: fix lints
yanksyoon Jun 29, 2025
93bb424
Merge branch 'main' into feat/prometheus-metrics
Jun 29, 2025
121795c
fix: lint
yanksyoon Jun 29, 2025
f7214a2
fix: lint
yanksyoon Jun 29, 2025
7ab740d
ci: remove unused setup script
yanksyoon Jun 30, 2025
022b3a8
docs: add changelog & increment version
yanksyoon Jun 30, 2025
549c61d
ci: separate test names
yanksyoon Jun 30, 2025
7152672
chore: refactor the metrics to be fetched without side effects
yanksyoon Jun 30, 2025
3e55bc1
chore: increment metrics by 1
yanksyoon Jun 30, 2025
af86e51
chore: move code
yanksyoon Jun 30, 2025
1e7b781
fix: lint fixe
yanksyoon Jun 30, 2025
b7be50b
fix integration test wf file
cbartz Jul 1, 2025
bd463bb
ci: update module name for prometheus metrics test
yanksyoon Jul 2, 2025
7ff9c70
ci: update cross controller test name
yanksyoon Jul 2, 2025
f53e31e
Update setup-integration-tests.sh
Jul 2, 2025
6ebf69b
fix: merge conflict fix
yanksyoon Jul 8, 2025
6bf81a7
test: test for busy/idle runner count (cleanup not happen when no vm)
yanksyoon Jul 9, 2025
6ae9a0d
test: remove duplicate test module
yanksyoon Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/e2e_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ jobs:
secrets: inherit
with:
juju-channel: 3.6/stable
pre-run-script: scripts/setup-integration-tests.sh
provider: lxd
test-tox-env: integration-juju3.6
modules: '["test_e2e"]'
Expand Down
19 changes: 9 additions & 10 deletions .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,34 @@ concurrency:
cancel-in-progress: true

jobs:
openstack-interface-tests-private-endpoint:
name: openstack interface test using private-endpoint
openstack-integration-tests-private-endpoint:
name: Integration test using private-endpoint
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
juju-channel: 3.6/stable
pre-run-script: scripts/setup-integration-tests.sh
provider: lxd
test-tox-env: integration-juju3.6
modules: '["test_runner_manager_openstack"]'
extra-arguments: '--log-format="%(asctime)s %(levelname)s %(message)s"'
modules: '["test_charm_metrics_failure", "test_charm_metrics_success", "test_charm_fork_repo", "test_charm_fork_path_change", "test_charm_no_runner", "test_charm_runner", "test_debug_ssh", "test_charm_upgrade", "test_reactive", "test_jobmanager_prespawned", "test_jobmanager_reactive"]'
extra-arguments: '-m openstack --log-format="%(asctime)s %(levelname)s %(message)s"'
self-hosted-runner: true
self-hosted-runner-label: stg-private-endpoint
openstack-integration-tests-private-endpoint:
name: Integration test using private-endpoint
openstack-integration-tests-cross-controller-private-endpoint:
name: Cross controller integration test using private-endpoint
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
juju-channel: 3.6/stable
pre-run-script: scripts/setup-integration-tests.sh
pre-run-script: tests/integration/setup-integration-tests.sh
provider: lxd
test-tox-env: integration-juju3.6
modules: '["test_charm_metrics_failure", "test_charm_metrics_success", "test_charm_fork_repo", "test_charm_fork_path_change", "test_charm_no_runner", "test_charm_runner", "test_debug_ssh", "test_charm_upgrade", "test_reactive", "test_jobmanager_prespawned", "test_jobmanager_reactive"]'
modules: '["test_prometheus_metrics"]'
extra-arguments: '-m openstack --log-format="%(asctime)s %(levelname)s %(message)s"'
self-hosted-runner: true
self-hosted-runner-label: stg-private-endpoint
allure-report:
if: ${{ (success() || failure()) && github.event_name == 'schedule' }}
needs:
- openstack-interface-tests-private-endpoint
- openstack-integration-tests-private-endpoint
- openstack-integration-tests-cross-controller-private-endpoint
uses: canonical/operator-workflows/.github/workflows/allure_report.yaml@main
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This changelog documents user-relevant changes to the GitHub runner charm.

### 2025-06-30
- New configuration options aproxy-exclude-addresses and aproxy-redirect-ports for allowing aproxy to redirect arbitrary TCP traffic
- Added prometheus metrics to the GitHub runner manager application.

## 2025-06-26

Expand Down
2 changes: 1 addition & 1 deletion github-runner-manager/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[project]
name = "github-runner-manager"
version = "0.5.0"
version = "0.6.0"
authors = [
{ name = "Canonical IS DevOps", email = "is-devops-team@canonical.com" },
]
Expand Down
5 changes: 3 additions & 2 deletions github-runner-manager/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
click==8.2.1
fabric >=3,<4
flask==3.1.1
ghapi
jinja2
kombu==5.5.3
openstacksdk==4.5.0
prometheus-client==0.22.1
pydantic < 2
pymongo==4.13.0
click==8.2.1
flask==3.1.1
17 changes: 14 additions & 3 deletions github-runner-manager/src/github_runner_manager/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from threading import Lock

from flask import Flask, request
from prometheus_client import generate_latest

from github_runner_manager.configuration import ApplicationConfiguration
from github_runner_manager.errors import CloudError, LockError
Expand Down Expand Up @@ -44,7 +45,7 @@ def check_runner() -> tuple[str, int]:
Returns:
Information on the runners in JSON format.
"""
app_config = app.config[APP_CONFIG_NAME]
app_config: ApplicationConfiguration = app.config[APP_CONFIG_NAME]
app.logger.info("Checking runners...")
runner_scaler = get_runner_scaler(app_config)
try:
Expand Down Expand Up @@ -72,7 +73,7 @@ def flush_runner() -> tuple[str, int]:
if flush_busy_str in ("True", "true"):
flush_busy = True

lock = get_lock()
lock = _get_lock()
with lock:
app.logger.info("Flushing runners...")
runner_scaler = get_runner_scaler(app_config)
Expand All @@ -87,7 +88,7 @@ def flush_runner() -> tuple[str, int]:
return ("", 204)


def get_lock() -> Lock:
def _get_lock() -> Lock:
"""Get the lock representing modification access to the set of runners.

Raises:
Expand All @@ -103,6 +104,16 @@ def get_lock() -> Lock:
raise LockError("Lock not configured")


@app.route("/metrics", methods=["GET"])
def metrics() -> bytes:
"""Return prometheus metrics from default registry.

Returns:
The latest metrics from the default Prometheus registry.
"""
return generate_latest()


@dataclass
class FlaskArgs:
"""Arguments for Flask HTTP server.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from github_runner_manager.metrics import events as metric_events
from github_runner_manager.metrics import github as github_metrics
from github_runner_manager.metrics import runner as runner_metrics
from github_runner_manager.metrics.reconcile import CLEANED_RUNNERS_TOTAL
from github_runner_manager.metrics.runner import RunnerMetrics
from github_runner_manager.openstack_cloud.constants import CREATE_SERVER_TIMEOUT
from github_runner_manager.platform.platform_provider import (
Expand Down Expand Up @@ -348,6 +349,7 @@ def _delete_cloud_runners(

logging.info("Delete runner in cloud: %s", cloud_runner.instance_id)
runner_metric = self._cloud.delete_runner(cloud_runner.instance_id)
CLEANED_RUNNERS_TOTAL.labels(self.manager_name).inc(1)
if not runner_metric:
logger.error("No metrics returned after deleting %s", cloud_runner.instance_id)
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
from dataclasses import dataclass

import github_runner_manager.reactive.runner_manager as reactive_runner_manager
from github_runner_manager.configuration import (
ApplicationConfiguration,
UserInfo,
)
from github_runner_manager.configuration import ApplicationConfiguration, UserInfo
from github_runner_manager.constants import GITHUB_SELF_HOSTED_ARCH_LABELS
from github_runner_manager.errors import (
CloudError,
Expand All @@ -28,6 +25,12 @@
RunnerMetadata,
)
from github_runner_manager.metrics import events as metric_events
from github_runner_manager.metrics.reconcile import (
BUSY_RUNNERS_COUNT,
EXPECTED_RUNNERS_COUNT,
IDLE_RUNNERS_COUNT,
RECONCILE_DURATION_SECONDS,
)
from github_runner_manager.openstack_cloud.models import OpenStackServerConfig
from github_runner_manager.openstack_cloud.openstack_runner_manager import (
OpenStackRunnerManager,
Expand Down Expand Up @@ -216,6 +219,8 @@ def __init__( # pylint: disable=too-many-arguments, too-many-positional-argumen
self._platform_name = platform_name
self._python_path = python_path

EXPECTED_RUNNERS_COUNT.labels(self._manager.manager_name).set(self._base_quantity)

def get_runner_info(self) -> RunnerInfo:
"""Get information on the runners.

Expand Down Expand Up @@ -321,7 +326,10 @@ def reconcile(self) -> int:
flavor=self._manager.manager_name,
expected_runner_quantity=expected_runner_quantity,
)
_issue_reconciliation_metric(reconcile_metric_data)
RECONCILE_DURATION_SECONDS.labels(self._manager.manager_name).observe(
end_timestamp - start_timestamp
)
_issue_reconciliation_metric(reconcile_metric_data, self._manager.manager_name)

logger.info("Finished reconciliation.")

Expand Down Expand Up @@ -403,12 +411,13 @@ def _log_runners(runner_list: tuple[RunnerInstance]) -> None:


def _issue_reconciliation_metric(
reconcile_metric_data: _ReconcileMetricData,
reconcile_metric_data: _ReconcileMetricData, manager_name: str
) -> None:
"""Issue the reconciliation metric.

Args:
reconcile_metric_data: The data used to issue the reconciliation metric.
manager_name: The name of the manager.
"""
idle_runners = {
runner.name
Expand All @@ -431,6 +440,9 @@ def _issue_reconciliation_metric(
logger.info("Current available runners (idle + healthy offline): %s", available_runners)
logger.info("Current active runners: %s", active_runners)

BUSY_RUNNERS_COUNT.labels(manager_name).set(len(active_runners))
IDLE_RUNNERS_COUNT.labels(manager_name).set(len(idle_runners))

try:

metric_events.issue_event(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""Module for collecting metrics related to the reconciliation process."""

from prometheus_client import Gauge, Histogram

LABEL_FLAVOR = "flavor"

RECONCILE_DURATION_SECONDS = Histogram(
name="reconcile_duration_seconds",
documentation="Duration of reconciliation (seconds)",
labelnames=[LABEL_FLAVOR],
)
EXPECTED_RUNNERS_COUNT = Gauge(
name="expected_runners_count",
documentation="Expected number of runners",
labelnames=[LABEL_FLAVOR],
)
BUSY_RUNNERS_COUNT = Gauge(
name="busy_runners_count",
documentation="Number of busy runners",
labelnames=[LABEL_FLAVOR],
)
IDLE_RUNNERS_COUNT = Gauge(
name="idle_runners_count",
documentation="Number of idle runners",
labelnames=[LABEL_FLAVOR],
)
CLEANED_RUNNERS_TOTAL = Gauge(
name="cleaned_runners_total",
documentation="Total number of runners cleaned up",
labelnames=[LABEL_FLAVOR],
)
6 changes: 0 additions & 6 deletions scripts/setup-integration-tests.sh

This file was deleted.

7 changes: 6 additions & 1 deletion src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,12 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._log_charm_status()

self._grafana_agent = COSAgentProvider(self)
self._grafana_agent = COSAgentProvider(
self,
metrics_endpoints=[
{"path": "/metrics", "port": int(manager_service.GITHUB_RUNNER_MANAGER_PORT)}
],
)

self._stored.set_default(
path=self.config[PATH_CONFIG_NAME], # for detecting changes
Expand Down
17 changes: 12 additions & 5 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import string
from pathlib import Path
from time import sleep
from typing import Any, AsyncIterator, Generator, Iterator, Optional, cast
from typing import Any, AsyncGenerator, AsyncIterator, Generator, Iterator, Optional, cast

import jubilant
import nest_asyncio
Expand Down Expand Up @@ -750,8 +750,10 @@ async def instance_helper_fixture(request: pytest.FixtureRequest) -> OpenStackIn
return OpenStackInstanceHelper(openstack_connection=openstack_connection)


@pytest.fixture(scope="module")
def juju(request: pytest.FixtureRequest, model: Model) -> Generator[jubilant.Juju, None, None]:
@pytest_asyncio.fixture(scope="module")
async def juju(
request: pytest.FixtureRequest, model: Model
) -> AsyncGenerator[jubilant.Juju, None]:
"""Pytest fixture that wraps :meth:`jubilant.with_model`."""

def show_debug_log(juju: jubilant.Juju):
Expand All @@ -764,14 +766,19 @@ def show_debug_log(juju: jubilant.Juju):
log = juju.debug_log(limit=1000)
print(log, end="")

controller = await model.get_controller()
if model:
juju = jubilant.Juju(model=model.name)
# Currently juju has no way of switching controller context, this is required to operate
# in the right controller's right model when using multiple controllers.
# See: https://github.com/canonical/jubilant/issues/158
juju = jubilant.Juju(model=f"{controller.controller_name}:{model.name}")
yield juju
show_debug_log(juju)
return

keep_models = cast(bool, request.config.getoption("--keep-models"))
with jubilant.temp_model(keep=keep_models) as juju:
with jubilant.temp_model(keep=keep_models, controller=controller.controller_name) as juju:
juju.model = f"{controller.controller_name}:{juju.model}"
juju.wait_timeout = 10 * 60
yield juju
show_debug_log(juju)
Expand Down
5 changes: 3 additions & 2 deletions tests/integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
GitPython>3,<4
pygithub
jubilant==1.1.*
kombu==5.*
pygithub
pymongo==4.*
jubilant==1.1.*
tenacity==9.1.2
30 changes: 30 additions & 0 deletions tests/integration/setup-integration-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash

# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

# Pre-run script for integration test operator-workflows action.
# https://github.com/canonical/operator-workflows/blob/main/.github/workflows/integration_test.yaml

# The COS observability stack are deployed on K8s models.

# save original controller that is used for testing
ORIGINAL_CONTROLLER=$(juju controllers --format json | jq -r '.controllers | keys | .[0]')

echo "bootstrapping microk8s juju controller"
sudo snap install microk8s --channel=1.32-strict/stable
GROUP=snap_microk8s
sudo usermod -a -G "$GROUP" "$USER"
if [ "$(id -gn)" != "$GROUP" ]; then
exec sg "$GROUP" "$0" "$*"
fi

# Get preferred source IP address for metallb
IPADDR=$( { ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc'; } )
sudo microk8s enable "metallb:$IPADDR-$IPADDR" "hostpath-storage"
microk8s status --wait-ready

unset JUJU_CONTROLLER
unset JUJU_MODEL
juju bootstrap microk8s microk8s
juju switch "$ORIGINAL_CONTROLLER"
Loading
Loading