44
55import controlsocket
66import configchangesocket
7+ import ipaddress
78import json
89import logging
910import secrets
11+ import socket
1012import urllib .parse
1113import yaml
1214
13- from charms .prometheus_k8s .v0 .prometheus_scrape import MetricsEndpointProvider
14- from charms .tempo_coordinator_k8s .v0 .tracing import TracingEndpointRequirer
15- from charms .certificate_transfer_interface .v1 .certificate_transfer import (
16- CertificateTransferRequires ,
17- )
18- from ops .charm import CharmBase , CollectStatusEvent
15+ from ops .charm import CharmBase , CollectStatusEvent , InstallEvent
1916from ops .framework import StoredState
20- from ops .charm import InstallEvent
2117from ops .main import main
2218from ops .model import ActiveStatus , BlockedStatus , Relation
2319from pathlib import Path
24- from typing import List
20+ from typing import List , Optional , Set
2521
2622logger = logging .getLogger (__name__ )
2723
@@ -40,15 +36,6 @@ class JujuControllerCharm(CharmBase):
4036 def __init__ (self , * args ):
4137 super ().__init__ (* args )
4238
43- self .tracing_requirer = TracingEndpointRequirer (
44- self ,
45- protocols = ["otlp_http" , "otlp_grpc" ],
46- relation_name = 'charm-tracing'
47- )
48- self ._certificate_transfer = CertificateTransferRequires (
49- self , relationship_name = 'charm-tracing-ca-cert'
50- )
51-
5239 self ._stored .set_default (
5340 last_bind_addresses = [],
5441 tracing_endpoints = {},
@@ -86,16 +73,21 @@ def _observe(self):
8673 self .framework .observe (
8774 self .on .dbcluster_relation_departed , self ._on_dbcluster_relation_departed )
8875 self .framework .observe (
89- self .tracing_requirer . on .endpoint_changed , self ._on_tracing_relation_changed )
76+ self .on .charm_tracing_relation_created , self ._on_tracing_relation_created )
9077 self .framework .observe (
91- self .tracing_requirer . on .endpoint_removed , self ._on_tracing_relation_removed )
78+ self .on .charm_tracing_relation_changed , self ._on_tracing_relation_changed )
9279 self .framework .observe (
93- self ._certificate_transfer .on .certificate_set_updated ,
94- self ._on_receive_ca_cert_updated ,
95- )
80+ self .on .charm_tracing_relation_broken , self ._on_tracing_relation_removed )
9681 self .framework .observe (
97- self ._certificate_transfer .on .certificates_removed , self ._on_receive_ca_cert_removed )
98- self ._metrics_endpoint = None
82+ self .on .charm_tracing_ca_cert_relation_created ,
83+ self ._on_certificate_relation_created )
84+ self .framework .observe (
85+ self .on .charm_tracing_ca_cert_relation_changed ,
86+ self ._on_certificate_relation_changed )
87+ self .framework .observe (
88+ self .on .charm_tracing_ca_cert_relation_broken ,
89+ self ._on_receive_ca_cert_removed )
90+ self .framework .observe (self .on .update_status , self ._on_metrics_refresh )
9991
10092 def _on_install (self , event : InstallEvent ):
10193 """Ensure that the controller configuration file exists."""
@@ -200,16 +192,61 @@ def _configure_metrics_endpoint(self, username, password):
200192 jobs = self ._metrics_jobs (username , password )
201193 if jobs is None :
202194 return
203- if self ._metrics_endpoint is None :
204- self ._metrics_endpoint = MetricsEndpointProvider (self , jobs = jobs )
205- self ._metrics_endpoint .set_scrape_job_spec ()
206- else :
207- self ._metrics_endpoint .update_scrape_job_spec (jobs )
195+ self ._set_metrics_scrape_data (
196+ self .model .relations ["metrics-endpoint" ], jobs = jobs )
208197
209198 def _configure_metrics_as_unit (self ):
210- if self ._metrics_endpoint is None :
211- self ._metrics_endpoint = MetricsEndpointProvider (self , jobs = [])
212- self ._metrics_endpoint .set_scrape_job_spec ()
199+ self ._set_metrics_scrape_data (
200+ self .model .relations ["metrics-endpoint" ], jobs = None )
201+
202+ def _set_metrics_scrape_data (self , relations , jobs : Optional [List [dict ]]):
203+ """Publish the prometheus_scrape relation data without the COSL-backed library."""
204+ for relation in relations :
205+ address , fqdn = self ._metrics_unit_address (relation )
206+ relation .data [self .unit ].update ({
207+ "prometheus_scrape_unit_address" : address ,
208+ "prometheus_scrape_unit_path" : "" ,
209+ "prometheus_scrape_unit_name" : self .unit .name ,
210+ "prometheus_scrape_unit_fqdn" : fqdn ,
211+ })
212+
213+ if not self .unit .is_leader () or jobs is None :
214+ return
215+
216+ metadata = json .dumps (self ._prometheus_scrape_metadata (), sort_keys = True )
217+ scrape_jobs = json .dumps (jobs , sort_keys = True )
218+ for relation in relations :
219+ relation .data [self .app ].update ({
220+ "scrape_metadata" : metadata ,
221+ "scrape_jobs" : scrape_jobs ,
222+ "alert_rules" : json .dumps ({}),
223+ })
224+
225+ def _metrics_unit_address (self , relation : Relation ):
226+ binding = self .model .get_binding (relation )
227+ network = getattr (binding , "network" , None )
228+ address = getattr (network , "bind_address" , None )
229+ if address and self ._is_valid_address (str (address )):
230+ return str (address ), socket .getfqdn ()
231+
232+ fqdn = socket .getfqdn ()
233+ return fqdn , fqdn
234+
235+ def _is_valid_address (self , address : str ) -> bool :
236+ try :
237+ ipaddress .ip_address (address )
238+ except ValueError :
239+ return False
240+ return True
241+
242+ def _prometheus_scrape_metadata (self ) -> dict :
243+ return {
244+ "model" : self .model .name ,
245+ "model_uuid" : str (self .model .uuid ),
246+ "application" : self .app .name ,
247+ "unit" : self .unit .name ,
248+ "charm_name" : self .meta .name ,
249+ }
213250
214251 def _remove_metrics_user (self , username ):
215252 try :
@@ -230,9 +267,9 @@ def _ensure_metrics_user(self, username, password):
230267 def _reconcile_metrics_as_leader (self , relations ):
231268 credentials = self ._metrics_credentials (relations )
232269 if credentials is None :
233- # MetricsEndpointProvider publishes one scrape job to every
234- # metrics-endpoint relation, so all Prometheus applications share
235- # one controller user. The oldest relation only seeds its name.
270+ # One scrape job is published to every metrics-endpoint relation,
271+ # so all Prometheus applications share one controller user. The
272+ # oldest relation only seeds its name.
236273 username = metrics_username (min (relations , key = lambda r : r .id ))
237274 password = generate_password ()
238275 else :
@@ -267,6 +304,18 @@ def _on_metrics_endpoint_relation_created(self, event):
267304 def _on_metrics_reconcile (self , _event ):
268305 self ._reconcile_metrics (self .model .relations ["metrics-endpoint" ])
269306
307+ def _on_metrics_refresh (self , _event ):
308+ relations = self .model .relations ["metrics-endpoint" ]
309+ if not relations :
310+ return
311+
312+ jobs = None
313+ if self .unit .is_leader ():
314+ credentials = self ._metrics_credentials (relations )
315+ if credentials :
316+ jobs = self ._metrics_jobs (* credentials )
317+ self ._set_metrics_scrape_data (relations , jobs )
318+
270319 def _on_metrics_endpoint_relation_broken (self , event ):
271320 relations = [
272321 relation for relation in self .model .relations ["metrics-endpoint" ]
@@ -299,14 +348,22 @@ def _on_dbcluster_relation_departed(self, event):
299348 relation = event .relation
300349 self ._update_bind_addresses (relation )
301350
351+ def _on_tracing_relation_created (self , event ):
352+ self ._request_tracing_protocols (event .relation )
353+
354+ def _request_tracing_protocols (self , relation : Relation ):
355+ if self .unit .is_leader ():
356+ relation .data [self .app ]["receivers" ] = json .dumps (["otlp_http" , "otlp_grpc" ])
357+
302358 def _on_tracing_relation_changed (self , event ):
303- if not self .tracing_requirer .is_ready (event .relation ):
359+ self ._request_tracing_protocols (event .relation )
360+ endpoints = self ._tracing_endpoints (event .relation )
361+ if not endpoints :
362+ if self ._stored .tracing_endpoints :
363+ self ._on_tracing_relation_removed (event )
304364 return
305365
306- self ._stored .tracing_endpoints = {
307- "otlp_grpc" : self .tracing_requirer .get_endpoint ("otlp_grpc" , event .relation ),
308- "otlp_http" : self .tracing_requirer .get_endpoint ("otlp_http" , event .relation ),
309- }
366+ self ._stored .tracing_endpoints = endpoints
310367 logger .info ("tracing endpoints updated: %s" , self ._stored .tracing_endpoints )
311368 self ._update_charm_tracing_config ()
312369
@@ -315,18 +372,76 @@ def _on_tracing_relation_removed(self, event):
315372 logger .info ("tracing endpoints cleared" )
316373 self ._update_charm_tracing_config ()
317374
318- def _on_receive_ca_cert_updated (self , event ):
319- ca_list = event .certificates
375+ def _tracing_endpoints (self , relation : Relation ) -> Optional [dict ]:
376+ if not relation .app :
377+ return None
378+
379+ try :
380+ receivers = json .loads (relation .data [relation .app ].get ("receivers" , "[]" ))
381+ except (json .JSONDecodeError , TypeError ):
382+ logger .info ("failed parsing tracing receivers for relation %s" , relation .id )
383+ return None
384+
385+ if not isinstance (receivers , list ):
386+ return None
387+
388+ endpoints = {}
389+ for receiver in receivers :
390+ if not isinstance (receiver , dict ):
391+ continue
392+
393+ protocol = receiver .get ("protocol" )
394+ name = protocol .get ("name" ) if isinstance (protocol , dict ) else protocol
395+ url = receiver .get ("url" )
396+ if name in ("otlp_grpc" , "otlp_http" ) and isinstance (url , str ):
397+ endpoints [name ] = url
398+
399+ return endpoints or None
400+
401+ def _on_certificate_relation_created (self , event ):
402+ if self .unit .is_leader ():
403+ event .relation .data [self .app ]["version" ] = json .dumps (1 )
404+
405+ def _on_certificate_relation_changed (self , event ):
406+ ca_list = self ._certificates_from_relation (event .relation )
320407 if not ca_list :
321408 return
322409
323410 self ._stored .ca_cert = '\n ' .join (sorted (ca_list ))
324- logger .info ("CA certificate updated from relation id %s" , event .relation_id )
411+ logger .info ("CA certificate updated from relation id %s" , event .relation . id )
325412 self ._update_charm_tracing_config ()
326413
414+ def _certificates_from_relation (self , relation : Relation ) -> Set [str ]:
415+ if relation .app :
416+ certificates = self ._json_string_set (
417+ relation .data [relation .app ].get ("certificates" ))
418+ if certificates :
419+ return certificates
420+
421+ for unit in relation .units :
422+ certificates = self ._json_string_set (relation .data [unit ].get ("chain" ))
423+ if certificates :
424+ return certificates
425+
426+ return set ()
427+
428+ def _json_string_set (self , value : Optional [str ]) -> Set [str ]:
429+ if not value :
430+ return set ()
431+
432+ try :
433+ data = json .loads (value )
434+ except (json .JSONDecodeError , TypeError ):
435+ return set ()
436+
437+ if not isinstance (data , list ):
438+ return set ()
439+
440+ return {item for item in data if isinstance (item , str )}
441+
327442 def _on_receive_ca_cert_removed (self , event ):
328443 self ._stored .ca_cert = None
329- logger .info ("CA certificate removed from relation id %s" , event .relation_id )
444+ logger .info ("CA certificate removed from relation id %s" , event .relation . id )
330445 self ._update_charm_tracing_config ()
331446
332447 def _update_bind_addresses (self , relation ):
0 commit comments