From c330307340bd1bc03196a037ebf4035e1d70d997 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Mon, 25 Nov 2024 10:51:54 +0100 Subject: [PATCH] forward grafana source UIDs --- lib/charms/grafana_k8s/v0/grafana_source.py | 24 +++++++- .../v1/prometheus_remote_write.py | 20 ++++++ src/charm.py | 16 ++--- tests/scenario/test_grafana_source.py | 61 +++++++++++++++++++ 4 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 tests/scenario/test_grafana_source.py diff --git a/lib/charms/grafana_k8s/v0/grafana_source.py b/lib/charms/grafana_k8s/v0/grafana_source.py index 6f2f4ef4..aa1e92a9 100644 --- a/lib/charms/grafana_k8s/v0/grafana_source.py +++ b/lib/charms/grafana_k8s/v0/grafana_source.py @@ -162,7 +162,7 @@ def __init__(self, *args): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 21 +LIBPATCH = 22 logger = logging.getLogger(__name__) @@ -429,6 +429,17 @@ def update_source(self, source_url: Optional[str] = ""): continue self._set_sources(rel) + def get_source_uids(self) -> Dict[str, Dict[str, str]]: + """Get the datasource UID(s) assigned by the remote end(s) to this datasource. + + Returns a mapping from remote application names to unit names to datasource uids.""" + uids = {} + for rel in self._charm.model.relations.get(self._relation_name, []): + if not rel: + continue + uids[rel.app.name] = json.loads(rel.data[rel.app]["datasource_uids"]) + return uids + def _set_sources_from_event(self, event: RelationJoinedEvent) -> None: """Get a `Relation` object from the event to pass on.""" self._set_sources(event.relation) @@ -551,6 +562,13 @@ def _on_grafana_peer_changed(self, _: RelationChangedEvent) -> None: self.on.sources_changed.emit() # pyright: ignore self.on.sources_to_delete_changed.emit() # pyright: ignore + def _publish_source_uids(self, rel: Relation, uids: Dict[str, str]): + """Share the datasource UIDs back to the datasources. + + Assumes only leader unit will call this method + """ + rel.data[self._charm.app]["datasource_uids"] = json.dumps(uids) + def _get_source_config(self, rel: Relation): """Generate configuration from data stored in relation data by providers.""" source_data = json.loads(rel.data[rel.app].get("grafana_source_data", "{}")) # type: ignore @@ -588,6 +606,10 @@ def _get_source_config(self, rel: Relation): sources_to_delete.remove(host_data["source_name"]) data.append(host_data) + + # share the unique source names back to the datasource units + self._publish_source_uids(rel, {ds["unit"]: ds["source_name"] for ds in data}) + self.set_peer_data("sources_to_delete", list(sources_to_delete)) return data diff --git a/lib/charms/prometheus_k8s/v1/prometheus_remote_write.py b/lib/charms/prometheus_k8s/v1/prometheus_remote_write.py index cf24b9f7..56bcad01 100644 --- a/lib/charms/prometheus_k8s/v1/prometheus_remote_write.py +++ b/lib/charms/prometheus_k8s/v1/prometheus_remote_write.py @@ -600,6 +600,7 @@ def __init__( *, server_url_func: Callable[[], str] = lambda: f"http://{socket.getfqdn()}:9090", endpoint_path: str = "/api/v1/write", + datasource_uids: Optional[Dict[str,Dict[str, str]]] = None ): """API to manage a provided relation with the `prometheus_remote_write` interface. @@ -609,6 +610,9 @@ def __init__( defined in metadata.yaml. server_url_func: A callable returning the URL for your prometheus server. endpoint_path: The path of the server's remote_write endpoint. + datasource_uids: The uids of the grafana datasources provisioned for these + prometheus units. A mapping from grafana applications to + local unit IDs to datasource UIDs (str). Raises: RelationNotFoundError: If there is no relation in the charm's metadata.yaml @@ -631,6 +635,11 @@ def __init__( self._get_server_url = server_url_func self._endpoint_path = endpoint_path + # we might have datasource relations with multiple grafana's. + # for each one of them, our datasource UID might be different + # (although, with the current implementation, it isn't). + self._datasource_uids = datasource_uids + on_relation = self._charm.on[self._relation_name] self.framework.observe( on_relation.relation_created, @@ -673,6 +682,17 @@ def update_endpoint(self, relation: Optional[Relation] = None) -> None: for relation in relations: self._set_endpoint_on_relation(relation) + if self._charm.unit.is_leader() and self._datasource_uids: + self._set_datasource_ids_on_relation(relation) + + def _set_datasource_ids_on_relation(self, relation: Relation) -> None: + """Set the remote_write endpoint on relations. + + Args: + relation: The relation whose data to update. + """ + relation.data[self._charm.app]["datasource_uids"] = json.dumps(self._datasource_uids) + def _set_endpoint_on_relation(self, relation: Relation) -> None: """Set the remote_write endpoint on relations. diff --git a/src/charm.py b/src/charm.py index 355b6474..1cc3ae82 100755 --- a/src/charm.py +++ b/src/charm.py @@ -201,13 +201,6 @@ def __init__(self, *args): ) self._prometheus_client = Prometheus(self.internal_url) - self.remote_write_provider = PrometheusRemoteWriteProvider( - charm=self, - relation_name=DEFAULT_REMOTE_WRITE_RELATION_NAME, - server_url_func=lambda: PrometheusCharm.external_url.fget(self), # type: ignore - endpoint_path="/api/v1/write", - ) - self.grafana_source_provider = GrafanaSourceProvider( charm=self, source_type="prometheus", @@ -221,6 +214,14 @@ def __init__(self, *args): extra_fields={"timeInterval": PROMETHEUS_GLOBAL_SCRAPE_INTERVAL}, ) + self.remote_write_provider = PrometheusRemoteWriteProvider( + charm=self, + relation_name=DEFAULT_REMOTE_WRITE_RELATION_NAME, + server_url_func=lambda: PrometheusCharm.external_url.fget(self), # type: ignore + endpoint_path="/api/v1/write", + datasource_uids=self.grafana_source_provider.get_source_uids() + ) + self.catalogue = CatalogueConsumer(charm=self, item=self._catalogue_item) self.charm_tracing = TracingEndpointRequirer( self, relation_name="charm-tracing", protocols=["otlp_http"] @@ -237,6 +238,7 @@ def __init__(self, *args): self.framework.observe(self.on.config_changed, self._configure) self.framework.observe(self.on.upgrade_charm, self._configure) self.framework.observe(self.on.update_status, self._update_status) + self.framework.observe(self.on.grafana_source_relation_changed, self._configure) self.framework.observe(self.ingress.on.ready_for_unit, self._on_ingress_ready) self.framework.observe(self.ingress.on.revoked_for_unit, self._on_ingress_revoked) self.framework.observe(self.cert_handler.on.cert_changed, self._on_server_cert_changed) diff --git a/tests/scenario/test_grafana_source.py b/tests/scenario/test_grafana_source.py new file mode 100644 index 00000000..2d61a2ed --- /dev/null +++ b/tests/scenario/test_grafana_source.py @@ -0,0 +1,61 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +import json + +import pytest +import yaml +from scenario import Container, ExecOutput, Relation, State + + +@pytest.mark.parametrize("this_app", ("prometheus", "prom")) +@pytest.mark.parametrize("this_unit_id", (0, 42)) +def test_remote_write_dashboard_uid_propagation(context, this_app, this_unit_id): + """Check that the grafana dashboard UIds are propagated over remote-write.""" + # GIVEN a remote-write relation + + remote_write_relation = Relation( + endpoint="receive-remote-write", + ) + + # AND a grafana-source relation + grafana_source_relation = Relation( + endpoint="grafana-source", + remote_app_name="grafana", + local_unit_data={ + "grafana_source_host": "some-hostname" + }, + local_app_data={ + "grafana_source_data": json.dumps( + {"model": "foo", "model_uuid": "bar", "application": "baz", "type": "tempo"} + ) + }, + + remote_app_data={ + # the datasources provisioned by grafana for this relation + "datasource_uids": json.dumps( + { + f"{this_app}/{this_unit_id}": f"juju_foo_bar_{this_app}_{this_unit_id}", + # some peer unit + f"{this_app}/{this_unit_id+1}": f"juju_foo_bar_{this_app}_{this_unit_id+1}", + } + ) + } + ) + + container = Container( + name="prometheus", + can_connect=True, + exec_mock={("update-ca-certificates", "--fresh"): ExecOutput(return_code=0, stdout="")}, + ) + state = State(leader=True, containers=[container], relations=[remote_write_relation, grafana_source_relation]) + + state_out = context.run(event=grafana_source_relation.changed_event, state=state) + + remote_write_out = state_out.get_relations("receive-remote-write")[0] + shared_ds_uids = remote_write_out.local_app_data.get("datasource_uids") + assert shared_ds_uids + + + +