@@ -211,14 +211,14 @@ def __init__(self, *args):
211
211
from collections import namedtuple
212
212
from itertools import chain
213
213
from pathlib import Path
214
- from typing import TYPE_CHECKING , Any , Callable , ClassVar , Dict , List , Optional , Set , Union
214
+ from typing import TYPE_CHECKING , Any , Callable , ClassVar , Dict , List , Optional , Set , Tuple , Union
215
215
216
216
import pydantic
217
217
from cosl import GrafanaDashboard , JujuTopology
218
218
from cosl .rules import AlertRules
219
219
from ops .charm import RelationChangedEvent
220
220
from ops .framework import EventBase , EventSource , Object , ObjectEvents
221
- from ops .model import Relation , Unit
221
+ from ops .model import Relation
222
222
from ops .testing import CharmType
223
223
224
224
if TYPE_CHECKING :
@@ -234,7 +234,7 @@ class _MetricsEndpointDict(TypedDict):
234
234
235
235
LIBID = "dc15fa84cef84ce58155fb84f6c6213a"
236
236
LIBAPI = 0
237
- LIBPATCH = 7
237
+ LIBPATCH = 8
238
238
239
239
PYDEPS = ["cosl" , "pydantic < 2" ]
240
240
@@ -258,7 +258,9 @@ class CosAgentProviderUnitData(pydantic.BaseModel):
258
258
metrics_alert_rules : dict
259
259
log_alert_rules : dict
260
260
dashboards : List [GrafanaDashboard ]
261
- subordinate : Optional [bool ]
261
+ # subordinate is no longer used but we should keep it until we bump the library to ensure
262
+ # we don't break compatibility.
263
+ subordinate : Optional [bool ] = None
262
264
263
265
# The following entries may vary across units of the same principal app.
264
266
# this data does not need to be forwarded to the gagent leader
@@ -277,9 +279,9 @@ class CosAgentPeersUnitData(pydantic.BaseModel):
277
279
# We need the principal unit name and relation metadata to be able to render identifiers
278
280
# (e.g. topology) on the leader side, after all the data moves into peer data (the grafana
279
281
# agent leader can only see its own principal, because it is a subordinate charm).
280
- principal_unit_name : str
281
- principal_relation_id : str
282
- principal_relation_name : str
282
+ unit_name : str
283
+ relation_id : str
284
+ relation_name : str
283
285
284
286
# The only data that is forwarded to the leader is data that needs to go into the app databags
285
287
# of the outgoing o11y relations.
@@ -299,7 +301,7 @@ def app_name(self) -> str:
299
301
TODO: Switch to using `model_post_init` when pydantic v2 is released?
300
302
https://github.com/pydantic/pydantic/issues/1729#issuecomment-1300576214
301
303
"""
302
- return self .principal_unit_name .split ("/" )[0 ]
304
+ return self .unit_name .split ("/" )[0 ]
303
305
304
306
305
307
class COSAgentProvider (Object ):
@@ -375,7 +377,6 @@ def _on_refresh(self, event):
375
377
dashboards = self ._dashboards ,
376
378
metrics_scrape_jobs = self ._scrape_jobs ,
377
379
log_slots = self ._log_slots ,
378
- subordinate = self ._charm .meta .subordinate ,
379
380
)
380
381
relation .data [self ._charm .unit ][data .KEY ] = data .json ()
381
382
except (
@@ -468,12 +469,6 @@ class COSAgentRequirerEvents(ObjectEvents):
468
469
validation_error = EventSource (COSAgentValidationError )
469
470
470
471
471
- class MultiplePrincipalsError (Exception ):
472
- """Custom exception for when there are multiple principal applications."""
473
-
474
- pass
475
-
476
-
477
472
class COSAgentRequirer (Object ):
478
473
"""Integration endpoint wrapper for the Requirer side of the cos_agent interface."""
479
474
@@ -559,13 +554,13 @@ def _on_relation_data_changed(self, event: RelationChangedEvent):
559
554
if not (provider_data := self ._validated_provider_data (raw )):
560
555
return
561
556
562
- # Copy data from the principal relation to the peer relation, so the leader could
557
+ # Copy data from the cos_agent relation to the peer relation, so the leader could
563
558
# follow up.
564
559
# Save the originating unit name, so it could be used for topology later on by the leader.
565
560
data = CosAgentPeersUnitData ( # peer relation databag model
566
- principal_unit_name = event .unit .name ,
567
- principal_relation_id = str (event .relation .id ),
568
- principal_relation_name = event .relation .name ,
561
+ unit_name = event .unit .name ,
562
+ relation_id = str (event .relation .id ),
563
+ relation_name = event .relation .name ,
569
564
metrics_alert_rules = provider_data .metrics_alert_rules ,
570
565
log_alert_rules = provider_data .log_alert_rules ,
571
566
dashboards = provider_data .dashboards ,
@@ -592,39 +587,7 @@ def trigger_refresh(self, _):
592
587
self .on .data_changed .emit () # pyright: ignore
593
588
594
589
@property
595
- def _principal_unit (self ) -> Optional [Unit ]:
596
- """Return the principal unit for a relation.
597
-
598
- Assumes that the relation is of type subordinate.
599
- Relies on the fact that, for subordinate relations, the only remote unit visible to
600
- *this unit* is the principal unit that this unit is attached to.
601
- """
602
- if relations := self ._principal_relations :
603
- # Technically it's a list, but for subordinates there can only be one relation
604
- principal_relation = next (iter (relations ))
605
- if units := principal_relation .units :
606
- # Technically it's a list, but for subordinates there can only be one
607
- return next (iter (units ))
608
-
609
- return None
610
-
611
- @property
612
- def _principal_relations (self ):
613
- relations = []
614
- for relation in self ._charm .model .relations [self ._relation_name ]:
615
- if not json .loads (relation .data [next (iter (relation .units ))]["config" ]).get (
616
- ["subordinate" ], False
617
- ):
618
- relations .append (relation )
619
- if len (relations ) > 1 :
620
- logger .error (
621
- "Multiple applications claiming to be principal. Update the cos-agent library in the client application charms."
622
- )
623
- raise MultiplePrincipalsError ("Multiple principal applications." )
624
- return relations
625
-
626
- @property
627
- def _remote_data (self ) -> List [CosAgentProviderUnitData ]:
590
+ def _remote_data (self ) -> List [Tuple [CosAgentProviderUnitData , JujuTopology ]]:
628
591
"""Return a list of remote data from each of the related units.
629
592
630
593
Assumes that the relation is of type subordinate.
@@ -641,7 +604,15 @@ def _remote_data(self) -> List[CosAgentProviderUnitData]:
641
604
continue
642
605
if not (provider_data := self ._validated_provider_data (raw )):
643
606
continue
644
- all_data .append (provider_data )
607
+
608
+ topology = JujuTopology (
609
+ model = self ._charm .model .name ,
610
+ model_uuid = self ._charm .model .uuid ,
611
+ application = unit .app .name ,
612
+ unit = unit .name ,
613
+ )
614
+
615
+ all_data .append ((provider_data , topology ))
645
616
646
617
return all_data
647
618
@@ -711,7 +682,7 @@ def metrics_alerts(self) -> Dict[str, Any]:
711
682
def metrics_jobs (self ) -> List [Dict ]:
712
683
"""Parse the relation data contents and extract the metrics jobs."""
713
684
scrape_jobs = []
714
- for data in self ._remote_data :
685
+ for data , topology in self ._remote_data :
715
686
for job in data .metrics_scrape_jobs :
716
687
# In #220, relation schema changed from a simplified dict to the standard
717
688
# `scrape_configs`.
@@ -727,6 +698,22 @@ def metrics_jobs(self) -> List[Dict]:
727
698
"tls_config" : {"insecure_skip_verify" : True },
728
699
}
729
700
701
+ # Apply labels to the scrape jobs
702
+ for static_config in job .get ("static_configs" , []):
703
+ topo_as_dict = topology .as_dict (excluded_keys = ["charm_name" ])
704
+ static_config ["labels" ] = {
705
+ # Be sure to keep labels from static_config
706
+ ** static_config .get ("labels" , {}),
707
+ # TODO: We should add a new method in juju_topology.py
708
+ # that like `as_dict` method, returns the keys with juju_ prefix
709
+ # https://github.com/canonical/cos-lib/issues/18
710
+ ** {
711
+ "juju_{}" .format (key ): value
712
+ for key , value in topo_as_dict .items ()
713
+ if value
714
+ },
715
+ }
716
+
730
717
scrape_jobs .append (job )
731
718
732
719
return scrape_jobs
@@ -735,7 +722,7 @@ def metrics_jobs(self) -> List[Dict]:
735
722
def snap_log_endpoints (self ) -> List [SnapEndpoint ]:
736
723
"""Fetch logging endpoints exposed by related snaps."""
737
724
plugs = []
738
- for data in self ._remote_data :
725
+ for data , _ in self ._remote_data :
739
726
targets = data .log_slots
740
727
if targets :
741
728
for target in targets :
@@ -775,7 +762,7 @@ def logs_alerts(self) -> Dict[str, Any]:
775
762
model = self ._charm .model .name ,
776
763
model_uuid = self ._charm .model .uuid ,
777
764
application = app_name ,
778
- # For the topology unit, we could use `data.principal_unit_name `, but that unit
765
+ # For the topology unit, we could use `data.unit_name `, but that unit
779
766
# name may not be very stable: `_gather_peer_data` de-duplicates by app name so
780
767
# the exact unit name that turns up first in the iterator may vary from time to
781
768
# time. So using the grafana-agent unit name instead.
@@ -808,9 +795,9 @@ def dashboards(self) -> List[Dict[str, str]]:
808
795
809
796
dashboards .append (
810
797
{
811
- "relation_id" : data .principal_relation_id ,
798
+ "relation_id" : data .relation_id ,
812
799
# We have the remote charm name - use it for the identifier
813
- "charm" : f"{ data .principal_relation_name } -{ app_name } " ,
800
+ "charm" : f"{ data .relation_name } -{ app_name } " ,
814
801
"content" : content ,
815
802
"title" : title ,
816
803
}
0 commit comments