diff --git a/charms/aodh-k8s/.sunbeam-build.yaml b/charms/aodh-k8s/.sunbeam-build.yaml index 3f334319..b75998db 100644 --- a/charms/aodh-k8s/.sunbeam-build.yaml +++ b/charms/aodh-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service templates: diff --git a/charms/aodh-k8s/metadata.yaml b/charms/aodh-k8s/metadata.yaml index 344f0708..3ac850a4 100644 --- a/charms/aodh-k8s/metadata.yaml +++ b/charms/aodh-k8s/metadata.yaml @@ -78,6 +78,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: aodh: diff --git a/charms/aodh-k8s/src/charm.py b/charms/aodh-k8s/src/charm.py index cc46e96b..cb5c730c 100755 --- a/charms/aodh-k8s/src/charm.py +++ b/charms/aodh-k8s/src/charm.py @@ -27,6 +27,7 @@ import ops.pebble import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.tracing as sunbeam_tracing from ops.framework import ( StoredState, ) @@ -43,6 +44,7 @@ AODH_LISTENER_CONTAINER = "aodh-listener" AODH_EXPIRER_CONTAINER = "aodh-expirer" +@sunbeam_tracing.trace_type class AODHWSGIPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): """Pebble handler for AODH api service.""" @@ -56,6 +58,7 @@ class AODHWSGIPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): super().init_service(context) +@sunbeam_tracing.trace_type class AODHEvaluatorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for AODH Evaluator.""" @@ -103,6 +106,7 @@ class AODHEvaluatorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): ] +@sunbeam_tracing.trace_type class AODHNotifierPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for AODH Notifier container.""" @@ -147,6 +151,7 @@ class AODHNotifierPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): ] +@sunbeam_tracing.trace_type class AODHListenerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for AODH Listener container.""" @@ -191,6 +196,7 @@ class AODHListenerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): ] +@sunbeam_tracing.trace_type class AODHExpirerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for AODH Expirer container.""" @@ -237,6 +243,7 @@ class AODHExpirerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): ] +@sunbeam_tracing.trace_sunbeam_charm class AodhOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/barbican-k8s/.sunbeam-build.yaml b/charms/barbican-k8s/.sunbeam-build.yaml index 20f18024..d6f573cf 100644 --- a/charms/barbican-k8s/.sunbeam-build.yaml +++ b/charms/barbican-k8s/.sunbeam-build.yaml @@ -5,6 +5,8 @@ external-libraries: - charms.vault_k8s.v0.vault_kv - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service - charms.keystone_k8s.v0.identity_resource diff --git a/charms/barbican-k8s/metadata.yaml b/charms/barbican-k8s/metadata.yaml index e7dc9be5..91e21ac0 100644 --- a/charms/barbican-k8s/metadata.yaml +++ b/charms/barbican-k8s/metadata.yaml @@ -48,6 +48,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/barbican-k8s/src/charm.py b/charms/barbican-k8s/src/charm.py index b120f2e4..c4f717f7 100755 --- a/charms/barbican-k8s/src/charm.py +++ b/charms/barbican-k8s/src/charm.py @@ -34,6 +34,7 @@ import ops_sunbeam.config_contexts as sunbeam_ctxts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from charms.vault_k8s.v0 import ( vault_kv, ) @@ -60,6 +61,7 @@ class NoRelationError(Exception): pass +@sunbeam_tracing.trace_type class WSGIBarbicanAdminConfigContext(sunbeam_ctxts.ConfigContext): """Configuration context for WSGI configuration.""" @@ -77,6 +79,7 @@ class WSGIBarbicanAdminConfigContext(sunbeam_ctxts.ConfigContext): } +@sunbeam_tracing.trace_type class VaultKvRequiresHandler(sunbeam_rhandlers.RelationHandler): """Handler for vault-kv relation.""" @@ -97,7 +100,7 @@ class VaultKvRequiresHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self) -> ops.Object: """Configure event handlers for a vault-kv relation.""" logger.debug("Setting up vault-kv event handler") - interface = vault_kv.VaultKvRequires( + interface = sunbeam_tracing.trace_type(vault_kv.VaultKvRequires)( self.charm, self.relation_name, self.mount_suffix, @@ -188,6 +191,7 @@ class VaultKvRequiresHandler(sunbeam_rhandlers.RelationHandler): } +@sunbeam_tracing.trace_type class BarbicanWorkerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Barbican worker.""" @@ -430,6 +434,7 @@ class BarbicanOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): return super().healthcheck_http_url + "?build" +@sunbeam_tracing.trace_sunbeam_charm class BarbicanVaultOperatorCharm(BarbicanOperatorCharm): """Vault specialized Barbican Operator Charm.""" diff --git a/charms/ceilometer-k8s/.sunbeam-build.yaml b/charms/ceilometer-k8s/.sunbeam-build.yaml index 05d8b2fe..376c6a84 100644 --- a/charms/ceilometer-k8s/.sunbeam-build.yaml +++ b/charms/ceilometer-k8s/.sunbeam-build.yaml @@ -2,6 +2,8 @@ external-libraries: - charms.rabbitmq_k8s.v0.rabbitmq - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v0.identity_credentials - charms.gnocchi_k8s.v0.gnocchi_service diff --git a/charms/ceilometer-k8s/metadata.yaml b/charms/ceilometer-k8s/metadata.yaml index 15ab0084..ac3f40ce 100644 --- a/charms/ceilometer-k8s/metadata.yaml +++ b/charms/ceilometer-k8s/metadata.yaml @@ -55,6 +55,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/ceilometer-k8s/src/charm.py b/charms/ceilometer-k8s/src/charm.py index aecdea4b..aafed989 100755 --- a/charms/ceilometer-k8s/src/charm.py +++ b/charms/ceilometer-k8s/src/charm.py @@ -31,6 +31,7 @@ import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from charms.ceilometer_k8s.v0.ceilometer_service import ( CeilometerConfigRequestEvent, CeilometerServiceProvides, @@ -55,6 +56,7 @@ CEILOMETER_CENTRAL_CONTAINER = "ceilometer-central" CEILOMETER_NOTIFICATION_CONTAINER = "ceilometer-notification" +@sunbeam_tracing.trace_type class GnocchiServiceRequiresHandler(sunbeam_rhandlers.RelationHandler): """Handle gnocchi service relation on the requires side.""" @@ -120,6 +122,7 @@ class GnocchiServiceRequiresHandler(sunbeam_rhandlers.RelationHandler): return self.interface.service_ready +@sunbeam_tracing.trace_type class CeilometerServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for ceilometer service relation.""" @@ -154,6 +157,7 @@ class CeilometerServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class CeilometerCentralPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for ceilometer-central service.""" @@ -189,6 +193,7 @@ class CeilometerCentralPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): return self.charm.container_configs +@sunbeam_tracing.trace_type class CeilometerNotificationPebbleHandler( sunbeam_chandlers.ServicePebbleHandler ): @@ -243,6 +248,7 @@ class CeilometerNotificationPebbleHandler( return _cconfigs +@sunbeam_tracing.trace_sunbeam_charm class CeilometerOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): """Charm the service.""" diff --git a/charms/cinder-ceph-k8s/.sunbeam-build.yaml b/charms/cinder-ceph-k8s/.sunbeam-build.yaml index f0d92878..5ec9cc92 100644 --- a/charms/cinder-ceph-k8s/.sunbeam-build.yaml +++ b/charms/cinder-ceph-k8s/.sunbeam-build.yaml @@ -3,6 +3,8 @@ external-libraries: - charms.rabbitmq_k8s.v0.rabbitmq - charms.traefik_k8s.v2.ingress - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v0.identity_credentials - charms.cinder_k8s.v0.storage_backend diff --git a/charms/cinder-ceph-k8s/metadata.yaml b/charms/cinder-ceph-k8s/metadata.yaml index 8e4c3464..10ef4381 100644 --- a/charms/cinder-ceph-k8s/metadata.yaml +++ b/charms/cinder-ceph-k8s/metadata.yaml @@ -45,6 +45,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: ceph-access: diff --git a/charms/cinder-ceph-k8s/src/charm.py b/charms/cinder-ceph-k8s/src/charm.py index f4d9eaf4..fc680f8f 100755 --- a/charms/cinder-ceph-k8s/src/charm.py +++ b/charms/cinder-ceph-k8s/src/charm.py @@ -39,6 +39,7 @@ import ops_sunbeam.core as core import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as relation_handlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.main import ( main, ) @@ -50,6 +51,7 @@ from ops.model import ( logger = logging.getLogger(__name__) +@sunbeam_tracing.trace_type class CephConfigurationContext(config_contexts.ConfigContext): """Configuration context to parse ceph parameters.""" @@ -68,6 +70,7 @@ class CephConfigurationContext(config_contexts.ConfigContext): return ctxt +@sunbeam_tracing.trace_type class CinderCephConfigurationContext(config_contexts.ConfigContext): """Configuration context for cinder parameters.""" @@ -94,13 +97,16 @@ class CinderCephConfigurationContext(config_contexts.ConfigContext): } +@sunbeam_tracing.trace_type class StorageBackendProvidesHandler(sunbeam_rhandlers.RelationHandler): """Relation handler for storage-backend interface type.""" def setup_event_handler(self): """Configure event handlers for an storage-backend relation.""" logger.debug("Setting up Identity Service event handler") - sb_svc = sunbeam_storage_backend.StorageBackendProvides( + sb_svc = sunbeam_tracing.trace_type( + sunbeam_storage_backend.StorageBackendProvides + )( self.charm, self.relation_name, ) @@ -119,6 +125,7 @@ class StorageBackendProvidesHandler(sunbeam_rhandlers.RelationHandler): return self.interface.remote_ready() +@sunbeam_tracing.trace_type class CephAccessProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for identity service relation.""" @@ -133,7 +140,9 @@ class CephAccessProvidesHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self): """Configure event handlers for an Identity service relation.""" logger.debug("Setting up Ceph Access event handler") - ceph_access_svc = sunbeam_ceph_access.CephAccessProvides( + ceph_access_svc = sunbeam_tracing.trace_type( + sunbeam_ceph_access.CephAccessProvides + )( self.charm, self.relation_name, ) @@ -155,6 +164,7 @@ class CephAccessProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class CinderVolumePebbleHandler(container_handlers.PebbleHandler): """Pebble handler for cinder-volume service.""" @@ -199,6 +209,7 @@ class CinderVolumePebbleHandler(container_handlers.PebbleHandler): self.start_service() +@sunbeam_tracing.trace_sunbeam_charm class CinderCephOperatorCharm(charm.OSBaseOperatorCharmK8S): """Cinder/Ceph Operator charm.""" diff --git a/charms/cinder-k8s/.sunbeam-build.yaml b/charms/cinder-k8s/.sunbeam-build.yaml index 814bd48e..ac1dbed5 100644 --- a/charms/cinder-k8s/.sunbeam-build.yaml +++ b/charms/cinder-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service templates: diff --git a/charms/cinder-k8s/metadata.yaml b/charms/cinder-k8s/metadata.yaml index a8ce6e06..dfdf4b66 100644 --- a/charms/cinder-k8s/metadata.yaml +++ b/charms/cinder-k8s/metadata.yaml @@ -63,6 +63,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/cinder-k8s/src/charm.py b/charms/cinder-k8s/src/charm.py index b4c48618..f8c9ec61 100755 --- a/charms/cinder-k8s/src/charm.py +++ b/charms/cinder-k8s/src/charm.py @@ -34,6 +34,7 @@ import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.main import ( main, ) @@ -45,6 +46,7 @@ CINDER_API_CONTAINER = "cinder-api" CINDER_SCHEDULER_CONTAINER = "cinder-scheduler" +@sunbeam_tracing.trace_type class CinderWSGIPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): """Pebble handler for Cinder WSGI services.""" @@ -101,6 +103,7 @@ class CinderWSGIPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): ] +@sunbeam_tracing.trace_type class CinderSchedulerPebbleHandler(sunbeam_chandlers.PebbleHandler): """Pebble handler for Cinder Scheduler services.""" @@ -159,13 +162,16 @@ class CinderSchedulerPebbleHandler(sunbeam_chandlers.PebbleHandler): ] +@sunbeam_tracing.trace_type class StorageBackendRequiresHandler(sunbeam_rhandlers.RelationHandler): """Relation handler for cinder storage backends.""" def setup_event_handler(self): """Configure event handlers for an Identity service relation.""" logger.debug("Setting up Identity Service event handler") - sb_svc = sunbeam_storage_backend.StorageBackendRequires( + sb_svc = sunbeam_tracing.trace_type( + sunbeam_storage_backend.StorageBackendRequires + )( self.charm, self.relation_name, ) @@ -188,6 +194,7 @@ class StorageBackendRequiresHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_sunbeam_charm class CinderOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/designate-bind-k8s/.sunbeam-build.yaml b/charms/designate-bind-k8s/.sunbeam-build.yaml index 728c49f6..5ae09e95 100644 --- a/charms/designate-bind-k8s/.sunbeam-build.yaml +++ b/charms/designate-bind-k8s/.sunbeam-build.yaml @@ -1,3 +1,5 @@ external-libraries: - charms.observability_libs.v1.kubernetes_service_patch - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing diff --git a/charms/designate-bind-k8s/metadata.yaml b/charms/designate-bind-k8s/metadata.yaml index d562253c..febe9867 100644 --- a/charms/designate-bind-k8s/metadata.yaml +++ b/charms/designate-bind-k8s/metadata.yaml @@ -35,6 +35,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/designate-bind-k8s/src/charm.py b/charms/designate-bind-k8s/src/charm.py index f858e2d4..88301506 100755 --- a/charms/designate-bind-k8s/src/charm.py +++ b/charms/designate-bind-k8s/src/charm.py @@ -40,6 +40,7 @@ import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.framework import ( StoredState, ) @@ -55,6 +56,7 @@ RNDC_REVISION_KEY = "rndc_revision" RNDC_STORE_KEY = "rndc-store" +@sunbeam_tracing.trace_type class BindPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for designate-bind service.""" @@ -74,6 +76,7 @@ class BindPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } +@sunbeam_tracing.trace_type class BindRndcProvidesRelationHandler(sunbeam_rhandlers.RelationHandler): """Handler for managing rndc clients.""" @@ -91,7 +94,9 @@ class BindRndcProvidesRelationHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self) -> ops.Object: """Setup event handler for the relation.""" - interface = bind_rndc.BindRndcProvides(self.charm, BIND_RNDC_RELATION) + interface = sunbeam_tracing.trace_type(bind_rndc.BindRndcProvides)( + self.charm, BIND_RNDC_RELATION + ) self.framework.observe( interface.on.new_bind_client_attached, self._on_bind_client_attached, @@ -188,6 +193,7 @@ class BindRndcProvidesRelationHandler(sunbeam_rhandlers.RelationHandler): } +@sunbeam_tracing.trace_sunbeam_charm class BindOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): """Charm the service.""" diff --git a/charms/designate-k8s/.sunbeam-build.yaml b/charms/designate-k8s/.sunbeam-build.yaml index caf4eb96..a99dd443 100644 --- a/charms/designate-k8s/.sunbeam-build.yaml +++ b/charms/designate-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service - charms.designate_bind_k8s.v0.bind_rndc diff --git a/charms/designate-k8s/metadata.yaml b/charms/designate-k8s/metadata.yaml index 53a40486..aa3dbd21 100644 --- a/charms/designate-k8s/metadata.yaml +++ b/charms/designate-k8s/metadata.yaml @@ -55,6 +55,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/designate-k8s/src/charm.py b/charms/designate-k8s/src/charm.py index 4dcf96a8..204c4dfc 100755 --- a/charms/designate-k8s/src/charm.py +++ b/charms/designate-k8s/src/charm.py @@ -39,6 +39,7 @@ import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing import tenacity from charms.designate_k8s.v0.designate_service import ( DesignateEndpointRequestEvent, @@ -62,6 +63,7 @@ class NoRelationError(Exception): pass +@sunbeam_tracing.trace_type class DesignatePebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): """Pebble handler for designate services.""" @@ -175,6 +177,7 @@ class DesignatePebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): super().init_service(context) +@sunbeam_tracing.trace_type class DesignateServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for designate service relation.""" @@ -189,7 +192,7 @@ class DesignateServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self): """Configure event handlers for an Ceilometer service relation.""" logger.debug("Setting up Ceilometer service event handler") - svc = DesignateServiceProvides( + svc = sunbeam_tracing.trace_type(DesignateServiceProvides)( self.charm, self.relation_name, ) @@ -211,6 +214,7 @@ class DesignateServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class BindRndcRequiresRelationHandler(sunbeam_rhandlers.RelationHandler): """Relation handler class.""" @@ -228,7 +232,9 @@ class BindRndcRequiresRelationHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self) -> ops.Object: """Setup event handler for the relation.""" - interface = bind_rndc.BindRndcRequires(self.charm, BIND_RNDC_RELATION) + interface = sunbeam_tracing.trace_type(bind_rndc.BindRndcRequires)( + self.charm, BIND_RNDC_RELATION + ) self.framework.observe( interface.on.connected, self._on_bind_rndc_connected, @@ -342,6 +348,7 @@ class BindRndcRequiresRelationHandler(sunbeam_rhandlers.RelationHandler): } +@sunbeam_tracing.trace_sunbeam_charm class DesignateOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/glance-k8s/.sunbeam-build.yaml b/charms/glance-k8s/.sunbeam-build.yaml index 23117802..78763119 100644 --- a/charms/glance-k8s/.sunbeam-build.yaml +++ b/charms/glance-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service templates: diff --git a/charms/glance-k8s/metadata.yaml b/charms/glance-k8s/metadata.yaml index 3608e67c..a5dda819 100644 --- a/charms/glance-k8s/metadata.yaml +++ b/charms/glance-k8s/metadata.yaml @@ -74,6 +74,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: image-service: diff --git a/charms/glance-k8s/src/charm.py b/charms/glance-k8s/src/charm.py index c34a7a10..2a923d79 100755 --- a/charms/glance-k8s/src/charm.py +++ b/charms/glance-k8s/src/charm.py @@ -35,6 +35,7 @@ import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from lightkube.core.client import ( Client, ) @@ -70,6 +71,7 @@ STORAGE_NAME = "local-repository" # and glance always interprets the mode-name as a requested version number. +@sunbeam_tracing.trace_type class GlanceAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Handler for glance api container.""" @@ -110,6 +112,7 @@ class GlanceAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): return super().init_service(context) +@sunbeam_tracing.trace_type class GlanceStorageRelationHandler(sunbeam_rhandlers.CephClientHandler): """A relation handler for optional glance storage relations. @@ -199,6 +202,7 @@ class GlanceStorageRelationHandler(sunbeam_rhandlers.CephClientHandler): return {} +@sunbeam_tracing.trace_type class GlanceConfigContext(sunbeam_ctxts.ConfigContext): """Glance configuration context.""" @@ -251,6 +255,7 @@ def bytes_from_string(value: str) -> int: raise ValueError(msg) +@sunbeam_tracing.trace_sunbeam_charm class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/gnocchi-k8s/.sunbeam-build.yaml b/charms/gnocchi-k8s/.sunbeam-build.yaml index ed80cd49..deec9cde 100644 --- a/charms/gnocchi-k8s/.sunbeam-build.yaml +++ b/charms/gnocchi-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service templates: diff --git a/charms/gnocchi-k8s/metadata.yaml b/charms/gnocchi-k8s/metadata.yaml index cffbcbf3..b4a52939 100644 --- a/charms/gnocchi-k8s/metadata.yaml +++ b/charms/gnocchi-k8s/metadata.yaml @@ -57,6 +57,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: gnocchi-service: diff --git a/charms/gnocchi-k8s/src/charm.py b/charms/gnocchi-k8s/src/charm.py index a3cd714e..7eb30746 100755 --- a/charms/gnocchi-k8s/src/charm.py +++ b/charms/gnocchi-k8s/src/charm.py @@ -31,6 +31,7 @@ import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from charms.gnocchi_k8s.v0.gnocchi_service import ( GnocchiServiceProvides, GnocchiServiceReadinessRequestEvent, @@ -53,6 +54,7 @@ GNOCHHI_WSGI_CONTAINER = "gnocchi-api" GNOCCHI_METRICD_CONTAINER = "gnocchi-metricd" +@sunbeam_tracing.trace_type class GnocchiServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for Gnocchi service relation on provider side.""" @@ -79,7 +81,7 @@ class GnocchiServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self): """Configure event handlers for Gnocchi service relation.""" logger.debug("Setting up Gnocchi service event handler") - svc = GnocchiServiceProvides( + svc = sunbeam_tracing.trace_type(GnocchiServiceProvides)( self.charm, self.relation_name, ) @@ -101,6 +103,7 @@ class GnocchiServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class GnocchiWSGIPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): """Pebble handler for Gnocchi WSGI services.""" @@ -139,6 +142,7 @@ class GnocchiWSGIPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): return _cconfigs +@sunbeam_tracing.trace_type class GnocchiMetricdPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Gnocchi metricd container.""" @@ -314,6 +318,7 @@ class GnocchiOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self.svc_ready_handler.interface.set_service_status(relation, True) +@sunbeam_tracing.trace_sunbeam_charm class GnocchiCephOperatorCharm(GnocchiOperatorCharm): """Charm the Gnocchi service with Ceph backend.""" diff --git a/charms/heat-k8s/.sunbeam-build.yaml b/charms/heat-k8s/.sunbeam-build.yaml index 051ed0c3..fad5201d 100644 --- a/charms/heat-k8s/.sunbeam-build.yaml +++ b/charms/heat-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_route_k8s.v0.traefik_route - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service - charms.keystone_k8s.v0.identity_resource diff --git a/charms/heat-k8s/metadata.yaml b/charms/heat-k8s/metadata.yaml index e5d7e8a8..72f3c136 100644 --- a/charms/heat-k8s/metadata.yaml +++ b/charms/heat-k8s/metadata.yaml @@ -60,6 +60,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/heat-k8s/src/charm.py b/charms/heat-k8s/src/charm.py index 7d31f6cd..7ed9db09 100755 --- a/charms/heat-k8s/src/charm.py +++ b/charms/heat-k8s/src/charm.py @@ -35,6 +35,7 @@ import ops_sunbeam.config_contexts as sunbeam_config_contexts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.framework import ( StoredState, ) @@ -56,6 +57,7 @@ HEAT_API_PORT = 8004 HEAT_API_CFN_PORT = 8000 +@sunbeam_tracing.trace_type class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Heat API container.""" @@ -97,6 +99,7 @@ class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } +@sunbeam_tracing.trace_type class HeatCfnAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Heat CFN API container.""" @@ -138,6 +141,7 @@ class HeatCfnAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } +@sunbeam_tracing.trace_type class HeatEnginePebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Heat engine container.""" @@ -162,6 +166,7 @@ class HeatEnginePebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } +@sunbeam_tracing.trace_type class HeatConfigurationContext(sunbeam_config_contexts.ConfigContext): """Heat configuration context.""" @@ -190,6 +195,7 @@ class HeatConfigurationContext(sunbeam_config_contexts.ConfigContext): } +@sunbeam_tracing.trace_sunbeam_charm class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/horizon-k8s/.sunbeam-build.yaml b/charms/horizon-k8s/.sunbeam-build.yaml index 0b1c032d..8acd2e81 100644 --- a/charms/horizon-k8s/.sunbeam-build.yaml +++ b/charms/horizon-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v0.identity_credentials templates: diff --git a/charms/horizon-k8s/metadata.yaml b/charms/horizon-k8s/metadata.yaml index 25ab1bca..0ec41265 100644 --- a/charms/horizon-k8s/metadata.yaml +++ b/charms/horizon-k8s/metadata.yaml @@ -49,6 +49,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: horizon: diff --git a/charms/horizon-k8s/src/charm.py b/charms/horizon-k8s/src/charm.py index 4d2dfb16..aaa33e62 100755 --- a/charms/horizon-k8s/src/charm.py +++ b/charms/horizon-k8s/src/charm.py @@ -32,6 +32,7 @@ import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.guard as sunbeam_guard +import ops_sunbeam.tracing as sunbeam_tracing from ops.main import ( main, ) @@ -84,6 +85,7 @@ def manage_plugins( return tag in out +@sunbeam_tracing.trace_type class WSGIHorizonPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): """Horizon Pebble Handler.""" @@ -117,6 +119,7 @@ class WSGIHorizonPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): ) +@sunbeam_tracing.trace_sunbeam_charm class HorizonOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/keystone-k8s/.sunbeam-build.yaml b/charms/keystone-k8s/.sunbeam-build.yaml index 6be3a7fe..0167ee0b 100644 --- a/charms/keystone-k8s/.sunbeam-build.yaml +++ b/charms/keystone-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing templates: - parts/section-database - parts/database-connection diff --git a/charms/keystone-k8s/metadata.yaml b/charms/keystone-k8s/metadata.yaml index 61e45cb4..28a59cb0 100644 --- a/charms/keystone-k8s/metadata.yaml +++ b/charms/keystone-k8s/metadata.yaml @@ -51,6 +51,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/keystone-k8s/src/charm.py b/charms/keystone-k8s/src/charm.py index c6245961..76d55551 100755 --- a/charms/keystone-k8s/src/charm.py +++ b/charms/keystone-k8s/src/charm.py @@ -59,6 +59,7 @@ import ops_sunbeam.core as sunbeam_core import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.job_ctrl as sunbeam_job_ctrl import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing import pwgen from charms.certificate_transfer_interface.v0.certificate_transfer import ( CertificateTransferProvides, @@ -96,6 +97,7 @@ KEYSTONE_CONF = "/etc/keystone/keystone.conf" LOGGING_CONF = "/etc/keystone/logging.conf" +@sunbeam_tracing.trace_type class KeystoneLoggingAdapter(sunbeam_contexts.ConfigContext): """Config adapter to collect logging config.""" @@ -117,6 +119,7 @@ class KeystoneLoggingAdapter(sunbeam_contexts.ConfigContext): return ctxt +@sunbeam_tracing.trace_type class KeystoneConfigAdapter(sunbeam_contexts.ConfigContext): """Config adapter to collect keystone config.""" @@ -150,6 +153,7 @@ class KeystoneConfigAdapter(sunbeam_contexts.ConfigContext): } +@sunbeam_tracing.trace_type class IdentityServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for identity service relation.""" @@ -186,6 +190,7 @@ class IdentityServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class DomainConfigHandler(sunbeam_rhandlers.RelationHandler): """Handler for domain config relation.""" @@ -228,6 +233,7 @@ class DomainConfigHandler(sunbeam_rhandlers.RelationHandler): return bool(self.get_domain_configs()) +@sunbeam_tracing.trace_type class IdentityCredentialsProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for identity credentials relation.""" @@ -264,6 +270,7 @@ class IdentityCredentialsProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class IdentityResourceProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for identity resource relation.""" @@ -298,6 +305,7 @@ class IdentityResourceProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class WSGIKeystonePebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): """Keystone Pebble Handler.""" @@ -316,6 +324,7 @@ class WSGIKeystonePebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): super().init_service(context) +@sunbeam_tracing.trace_sunbeam_charm(extra_types=(manager.KeystoneManager,)) class KeystoneOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/keystone-ldap-k8s/.sunbeam-build.yaml b/charms/keystone-ldap-k8s/.sunbeam-build.yaml index 0c73733d..9c872b9b 100644 --- a/charms/keystone-ldap-k8s/.sunbeam-build.yaml +++ b/charms/keystone-ldap-k8s/.sunbeam-build.yaml @@ -1,4 +1,6 @@ external-libraries: - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v0.domain_config diff --git a/charms/keystone-ldap-k8s/metadata.yaml b/charms/keystone-ldap-k8s/metadata.yaml index e52645bc..b131776f 100644 --- a/charms/keystone-ldap-k8s/metadata.yaml +++ b/charms/keystone-ldap-k8s/metadata.yaml @@ -16,3 +16,7 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 diff --git a/charms/keystone-ldap-k8s/src/charm.py b/charms/keystone-ldap-k8s/src/charm.py index 26d3ae74..473123e8 100755 --- a/charms/keystone-ldap-k8s/src/charm.py +++ b/charms/keystone-ldap-k8s/src/charm.py @@ -35,6 +35,7 @@ import ops.charm import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.config_contexts as config_contexts import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.main import ( main, ) @@ -43,6 +44,7 @@ from ops.main import ( logger = logging.getLogger(__name__) +@sunbeam_tracing.trace_type class LDAPConfigContext(config_contexts.ConfigContext): """Configuration context for cinder parameters.""" @@ -58,6 +60,7 @@ class LDAPConfigContext(config_contexts.ConfigContext): return {"config": config} +@sunbeam_tracing.trace_type class DomainConfigProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for identity credentials relation.""" @@ -72,7 +75,9 @@ class DomainConfigProvidesHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self): """Configure event handlers for a domain config relation.""" logger.debug("Setting up domain config event handler") - self.domain_config = sunbeam_dc_svc.DomainConfigProvides( + self.domain_config = sunbeam_tracing.trace_type( + sunbeam_dc_svc.DomainConfigProvides + )( self.charm, self.relation_name, ) @@ -92,6 +97,7 @@ class DomainConfigProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_sunbeam_charm class KeystoneLDAPK8SCharm(sunbeam_charm.OSBaseOperatorCharm): """Charm the service.""" diff --git a/charms/magnum-k8s/.sunbeam-build.yaml b/charms/magnum-k8s/.sunbeam-build.yaml index 62836904..3efdaa05 100644 --- a/charms/magnum-k8s/.sunbeam-build.yaml +++ b/charms/magnum-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service - charms.keystone_k8s.v0.identity_resource diff --git a/charms/magnum-k8s/metadata.yaml b/charms/magnum-k8s/metadata.yaml index 67597a06..a72d3efa 100644 --- a/charms/magnum-k8s/metadata.yaml +++ b/charms/magnum-k8s/metadata.yaml @@ -57,6 +57,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/magnum-k8s/src/charm.py b/charms/magnum-k8s/src/charm.py index 9647adb4..283deb57 100755 --- a/charms/magnum-k8s/src/charm.py +++ b/charms/magnum-k8s/src/charm.py @@ -29,6 +29,7 @@ import ops_sunbeam.config_contexts as sunbeam_config_contexts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.framework import ( StoredState, ) @@ -43,6 +44,7 @@ MAGNUM_API_CONTAINER = "magnum-api" MAGNUM_CONDUCTOR_CONTAINER = "magnum-conductor" +@sunbeam_tracing.trace_type class MagnumConfigurationContext(sunbeam_config_contexts.ConfigContext): """Magnum configuration context.""" @@ -67,6 +69,7 @@ class MagnumConfigurationContext(sunbeam_config_contexts.ConfigContext): } +@sunbeam_tracing.trace_type class MagnumConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for magnum worker.""" @@ -134,6 +137,7 @@ class MagnumConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): return self.pebble_ready +@sunbeam_tracing.trace_sunbeam_charm class MagnumOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/neutron-k8s/.sunbeam-build.yaml b/charms/neutron-k8s/.sunbeam-build.yaml index 58e86aec..97d4b589 100644 --- a/charms/neutron-k8s/.sunbeam-build.yaml +++ b/charms/neutron-k8s/.sunbeam-build.yaml @@ -5,6 +5,8 @@ external-libraries: - charms.tls_certificates_interface.v3.tls_certificates - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service - charms.ovn_central_k8s.v0.ovsdb diff --git a/charms/neutron-k8s/metadata.yaml b/charms/neutron-k8s/metadata.yaml index 35b335f4..090077fa 100644 --- a/charms/neutron-k8s/metadata.yaml +++ b/charms/neutron-k8s/metadata.yaml @@ -66,6 +66,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/neutron-k8s/src/charm.py b/charms/neutron-k8s/src/charm.py index 0fc3945d..09b7d6b4 100755 --- a/charms/neutron-k8s/src/charm.py +++ b/charms/neutron-k8s/src/charm.py @@ -36,6 +36,7 @@ import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.job_ctrl as sunbeam_job_ctrl import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.framework import ( StoredState, ) @@ -49,6 +50,7 @@ from ops.model import ( logger = logging.getLogger(__name__) +@sunbeam_tracing.trace_type class DesignateServiceRequiresHandler(sunbeam_rhandlers.RelationHandler): """Handle external-dns relation on the requires side.""" @@ -80,7 +82,9 @@ class DesignateServiceRequiresHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self) -> None: """Configure event handlers for external-dns service relation.""" logger.debug("Setting up Designate service event handler") - svc = designate_svc.DesignateServiceRequires( + svc = sunbeam_tracing.trace_type( + designate_svc.DesignateServiceRequires + )( self.charm, self.relation_name, ) @@ -117,6 +121,7 @@ class DesignateServiceRequiresHandler(sunbeam_rhandlers.RelationHandler): return False +@sunbeam_tracing.trace_type class NeutronServerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Handler for interacting with pebble data.""" @@ -345,6 +350,7 @@ class NeutronOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): # Neutron OVN Specific Code +@sunbeam_tracing.trace_type class OVNContext(sunbeam_ctxts.ConfigContext): """OVN configuration.""" @@ -380,6 +386,7 @@ class OVNContext(sunbeam_ctxts.ConfigContext): } +@sunbeam_tracing.trace_type class NeutronServerOVNPebbleHandler(NeutronServerPebbleHandler): """Handler for interacting with neutron container.""" @@ -419,6 +426,7 @@ class NeutronServerOVNPebbleHandler(NeutronServerPebbleHandler): ] +@sunbeam_tracing.trace_sunbeam_charm class NeutronOVNOperatorCharm(NeutronOperatorCharm): """Neutron charm class for OVN.""" diff --git a/charms/nova-k8s/.sunbeam-build.yaml b/charms/nova-k8s/.sunbeam-build.yaml index 4463988c..ac9eaf37 100644 --- a/charms/nova-k8s/.sunbeam-build.yaml +++ b/charms/nova-k8s/.sunbeam-build.yaml @@ -6,6 +6,8 @@ external-libraries: - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api - charms.sunbeam_nova_compute_operator.v0.cloud_compute + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service templates: diff --git a/charms/nova-k8s/metadata.yaml b/charms/nova-k8s/metadata.yaml index 7895d6aa..4058fc5b 100644 --- a/charms/nova-k8s/metadata.yaml +++ b/charms/nova-k8s/metadata.yaml @@ -99,6 +99,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: nova-service: diff --git a/charms/nova-k8s/src/charm.py b/charms/nova-k8s/src/charm.py index 58211d57..87106a4e 100755 --- a/charms/nova-k8s/src/charm.py +++ b/charms/nova-k8s/src/charm.py @@ -34,6 +34,7 @@ import ops_sunbeam.config_contexts as sunbeam_ctxts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from charms.nova_k8s.v0.nova_service import ( NovaConfigRequestEvent, NovaServiceProvides, @@ -59,6 +60,7 @@ NOVA_SPICEPROXY_INGRESS_NAME = "nova-spiceproxy" NOVA_SPICEPROXY_INGRESS_PORT = 6082 +@sunbeam_tracing.trace_type class WSGINovaMetadataConfigContext(sunbeam_ctxts.ConfigContext): """Configuration context for WSGI configuration.""" @@ -76,6 +78,7 @@ class WSGINovaMetadataConfigContext(sunbeam_ctxts.ConfigContext): } +@sunbeam_tracing.trace_type class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Nova scheduler.""" @@ -133,6 +136,7 @@ class NovaSchedulerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): return self.pebble_ready +@sunbeam_tracing.trace_type class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Nova Conductor container.""" @@ -176,6 +180,7 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): ] +@sunbeam_tracing.trace_type class NovaSpiceProxyPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Nova spice proxy.""" @@ -233,6 +238,7 @@ class NovaSpiceProxyPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): return self.pebble_ready +@sunbeam_tracing.trace_type class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler): """Handles the cloud-compute relation on the requires side.""" @@ -267,7 +273,9 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self): """Configure event handlers for the cloud-compute service relation.""" logger.debug("Setting up cloud-compute event handler") - compute_service = cloud_compute.CloudComputeRequires( + compute_service = sunbeam_tracing.trace_type( + cloud_compute.CloudComputeRequires + )( self.charm, self.relation_name, ) @@ -293,6 +301,7 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class NovaServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): """Handler for nova service relation.""" @@ -307,7 +316,7 @@ class NovaServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self): """Configure event handlers for nova service relation.""" logger.debug("Setting up Nova service event handler") - svc = NovaServiceProvides( + svc = sunbeam_tracing.trace_type(NovaServiceProvides)( self.charm, self.relation_name, ) @@ -327,6 +336,7 @@ class NovaServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_sunbeam_charm class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/octavia-k8s/.sunbeam-build.yaml b/charms/octavia-k8s/.sunbeam-build.yaml index 0262a581..7d9e9e1d 100644 --- a/charms/octavia-k8s/.sunbeam-build.yaml +++ b/charms/octavia-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.tls_certificates_interface.v3.tls_certificates - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service - charms.keystone_k8s.v0.identity_resource diff --git a/charms/octavia-k8s/metadata.yaml b/charms/octavia-k8s/metadata.yaml index 5b5b6f74..90efbcd1 100644 --- a/charms/octavia-k8s/metadata.yaml +++ b/charms/octavia-k8s/metadata.yaml @@ -82,6 +82,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 peers: peers: diff --git a/charms/octavia-k8s/src/charm.py b/charms/octavia-k8s/src/charm.py index cadb6c9e..25f4cd34 100755 --- a/charms/octavia-k8s/src/charm.py +++ b/charms/octavia-k8s/src/charm.py @@ -33,6 +33,7 @@ import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.framework import ( StoredState, ) @@ -47,6 +48,7 @@ OCTAVIA_HOUSEKEEPING_CONTAINER = "octavia-housekeeping" OCTAVIA_AGENT_SOCKET_DIR = "/var/run/octavia" +@sunbeam_tracing.trace_type class OctaviaDriverAgentPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Octavia Driver Agent.""" @@ -72,6 +74,7 @@ class OctaviaDriverAgentPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } +@sunbeam_tracing.trace_type class OctaviaHousekeepingPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for Octavia Housekeeping.""" @@ -97,6 +100,7 @@ class OctaviaHousekeepingPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } +@sunbeam_tracing.trace_type class OVNContext(sunbeam_config_contexts.ConfigContext): """OVN configuration.""" @@ -295,6 +299,7 @@ class OctaviaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): return ops +@sunbeam_tracing.trace_sunbeam_charm class OctaviaOVNOperatorCharm(OctaviaOperatorCharm): """Charm the Octavia service with OVN provider.""" diff --git a/charms/openstack-exporter-k8s/.sunbeam-build.yaml b/charms/openstack-exporter-k8s/.sunbeam-build.yaml index 7fd3ca03..b69953e0 100644 --- a/charms/openstack-exporter-k8s/.sunbeam-build.yaml +++ b/charms/openstack-exporter-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.tls_certificates_interface.v3.tls_certificates - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v0.identity_resource templates: diff --git a/charms/openstack-exporter-k8s/metadata.yaml b/charms/openstack-exporter-k8s/metadata.yaml index f0332ae6..9218515f 100644 --- a/charms/openstack-exporter-k8s/metadata.yaml +++ b/charms/openstack-exporter-k8s/metadata.yaml @@ -34,6 +34,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: metrics-endpoint: diff --git a/charms/openstack-exporter-k8s/src/charm.py b/charms/openstack-exporter-k8s/src/charm.py index 04febfb5..28d02afa 100755 --- a/charms/openstack-exporter-k8s/src/charm.py +++ b/charms/openstack-exporter-k8s/src/charm.py @@ -33,6 +33,7 @@ import ops_sunbeam.config_contexts as sunbeam_config_contexts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.main import ( main, ) @@ -43,6 +44,7 @@ CONFIGURE_SECRET_PREFIX = "configure-" CONTAINER = "openstack-exporter" +@sunbeam_tracing.trace_type class OSExporterConfigurationContext(sunbeam_config_contexts.ConfigContext): """OSExporter configuration context.""" @@ -75,6 +77,7 @@ class OSExporterConfigurationContext(sunbeam_config_contexts.ConfigContext): } +@sunbeam_tracing.trace_type class OSExporterPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for the container.""" @@ -107,6 +110,7 @@ class OSExporterPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } +@sunbeam_tracing.trace_type class MetricsEndpointRelationHandler(sunbeam_rhandlers.RelationHandler): """Relation handler for Metrics Endpoint relation.""" @@ -117,9 +121,9 @@ class MetricsEndpointRelationHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self) -> ops.Object: """Configure event handlers for the relation.""" logger.debug("Setting up Metrics Endpoint event handler") - interface = prometheus_scrape.MetricsEndpointProvider( - self.charm, jobs=self.charm._scrape_jobs - ) + interface = sunbeam_tracing.trace_type( + prometheus_scrape.MetricsEndpointProvider + )(self.charm, jobs=self.charm._scrape_jobs) return interface @@ -129,6 +133,7 @@ class MetricsEndpointRelationHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class GrafanaDashboardsRelationHandler(sunbeam_rhandlers.RelationHandler): """Relation handler for Grafana Dashboards relation.""" @@ -137,7 +142,9 @@ class GrafanaDashboardsRelationHandler(sunbeam_rhandlers.RelationHandler): def setup_event_handler(self) -> ops.Object: """Configure event handlers for the relation.""" logger.debug("Setting up Grafana Dashboard event handler") - interface = grafana_dashboard.GrafanaDashboardProvider( + interface = sunbeam_tracing.trace_type( + grafana_dashboard.GrafanaDashboardProvider + )( self.charm, ) @@ -149,6 +156,7 @@ class GrafanaDashboardsRelationHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_sunbeam_charm class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): """Charm the service.""" diff --git a/charms/openstack-hypervisor/.sunbeam-build.yaml b/charms/openstack-hypervisor/.sunbeam-build.yaml index 28d4ecea..294bb11d 100644 --- a/charms/openstack-hypervisor/.sunbeam-build.yaml +++ b/charms/openstack-hypervisor/.sunbeam-build.yaml @@ -7,6 +7,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.tls_certificates_interface.v3.tls_certificates - charms.certificate_transfer_interface.v0.certificate_transfer + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v0.identity_credentials - charms.ovn_central_k8s.v0.ovsdb diff --git a/charms/openstack-hypervisor/metadata.yaml b/charms/openstack-hypervisor/metadata.yaml index ec3e5e48..d49dd20d 100644 --- a/charms/openstack-hypervisor/metadata.yaml +++ b/charms/openstack-hypervisor/metadata.yaml @@ -28,6 +28,10 @@ requires: optional: true nova-service: interface: nova + tracing: + interface: tracing + optional: true + limit: 1 provides: cos-agent: diff --git a/charms/openstack-hypervisor/src/charm.py b/charms/openstack-hypervisor/src/charm.py index 259dbf6c..5846cb57 100755 --- a/charms/openstack-hypervisor/src/charm.py +++ b/charms/openstack-hypervisor/src/charm.py @@ -40,6 +40,7 @@ import ops_sunbeam.core as sunbeam_core import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.ovn.relation_handlers as ovn_relation_handlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from charms.ceilometer_k8s.v0.ceilometer_service import ( CeilometerConfigChangedEvent, CeilometerServiceGoneAwayEvent, @@ -70,6 +71,7 @@ MIGRATION_BINDING = "migration" MTLS_USAGES = {x509.OID_SERVER_AUTH, x509.OID_CLIENT_AUTH} +@sunbeam_tracing.trace_type class MTlsCertificatesHandler(sunbeam_rhandlers.TlsCertificatesHandler): """Handler for certificates interface.""" @@ -143,6 +145,7 @@ class MTlsCertificatesHandler(sunbeam_rhandlers.TlsCertificatesHandler): return {} +@sunbeam_tracing.trace_sunbeam_charm(extra_types=(snap.SnapCache, snap.Snap)) class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm): """Charm the service.""" diff --git a/charms/openstack-images-sync-k8s/.sunbeam-build.yaml b/charms/openstack-images-sync-k8s/.sunbeam-build.yaml index 1ea54213..36d633d5 100644 --- a/charms/openstack-images-sync-k8s/.sunbeam-build.yaml +++ b/charms/openstack-images-sync-k8s/.sunbeam-build.yaml @@ -1,5 +1,7 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service diff --git a/charms/openstack-images-sync-k8s/charmcraft.yaml b/charms/openstack-images-sync-k8s/charmcraft.yaml index 69704b25..e95d41c9 100644 --- a/charms/openstack-images-sync-k8s/charmcraft.yaml +++ b/charms/openstack-images-sync-k8s/charmcraft.yaml @@ -17,6 +17,20 @@ bases: - name: ubuntu channel: "22.04" +parts: + charm: + build-packages: + - git + - libffi-dev + - libssl-dev + - pkg-config + - rustc + - cargo + charm-binary-python-packages: + - cryptography + - jsonschema + - jinja2 + config: options: debug: @@ -76,3 +90,7 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 diff --git a/charms/openstack-images-sync-k8s/src/charm.py b/charms/openstack-images-sync-k8s/src/charm.py index e05aa6e6..594b1a21 100755 --- a/charms/openstack-images-sync-k8s/src/charm.py +++ b/charms/openstack-images-sync-k8s/src/charm.py @@ -31,6 +31,7 @@ import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.config_contexts as sunbeam_config_contexts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.tracing as sunbeam_tracing from charms.keystone_k8s.v1.identity_service import ( IdentityServiceRequires, ) @@ -56,6 +57,7 @@ def _frequency_to_seconds(frequency: str) -> int: raise ValueError(f"Unknown frequency {frequency!r}") +@sunbeam_tracing.trace_type class SyncCharmConfigContext(sunbeam_config_contexts.CharmConfigContext): """Configure context for templates.""" @@ -72,6 +74,7 @@ class SyncCharmConfigContext(sunbeam_config_contexts.CharmConfigContext): } +@sunbeam_tracing.trace_type class HttpSyncConfigContext(sunbeam_config_contexts.ConfigContext): """Configuration context for the http sync service.""" @@ -88,6 +91,7 @@ class HttpSyncConfigContext(sunbeam_config_contexts.ConfigContext): } +@sunbeam_tracing.trace_type class OpenstackImagesSyncPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Handler for openstack images sync container.""" @@ -146,6 +150,7 @@ class OpenstackImagesSyncPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): return super().init_service(context) +@sunbeam_tracing.trace_sunbeam_charm class OpenstackImagesSyncK8SCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the application.""" diff --git a/charms/ovn-central-k8s/.sunbeam-build.yaml b/charms/ovn-central-k8s/.sunbeam-build.yaml index 40aeb118..12acf013 100644 --- a/charms/ovn-central-k8s/.sunbeam-build.yaml +++ b/charms/ovn-central-k8s/.sunbeam-build.yaml @@ -1,3 +1,5 @@ external-libraries: - charms.tls_certificates_interface.v3.tls_certificates - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing diff --git a/charms/ovn-central-k8s/metadata.yaml b/charms/ovn-central-k8s/metadata.yaml index 85220c75..2a649ef8 100644 --- a/charms/ovn-central-k8s/metadata.yaml +++ b/charms/ovn-central-k8s/metadata.yaml @@ -72,6 +72,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: ovsdb: diff --git a/charms/ovn-central-k8s/src/charm.py b/charms/ovn-central-k8s/src/charm.py index 7158c4a7..2345a757 100755 --- a/charms/ovn-central-k8s/src/charm.py +++ b/charms/ovn-central-k8s/src/charm.py @@ -35,6 +35,7 @@ import ops_sunbeam.ovn.config_contexts as ovn_ctxts import ops_sunbeam.ovn.container_handlers as ovn_chandlers import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing import ovn import ovsdb as ch_ovsdb import tenacity @@ -53,6 +54,7 @@ OVN_NORTHD_CONTAINER = "ovn-northd" OVN_DB_CONTAINERS = [OVN_SB_DB_CONTAINER, OVN_NB_DB_CONTAINER] +@sunbeam_tracing.trace_type class OVNNorthBPebbleHandler(ovn_chandlers.OVNPebbleHandler): """Handler for North OVN DB.""" @@ -82,6 +84,7 @@ class OVNNorthBPebbleHandler(ovn_chandlers.OVNPebbleHandler): return _cc +@sunbeam_tracing.trace_type class OVNNorthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler): """Handler for North-bound OVN DB.""" @@ -129,6 +132,7 @@ class OVNNorthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler): } +@sunbeam_tracing.trace_type class OVNSouthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler): """Handler for South-bound OVN DB.""" @@ -176,6 +180,7 @@ class OVNSouthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler): } +@sunbeam_tracing.trace_sunbeam_charm class OVNCentralOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): """Charm the service.""" diff --git a/charms/ovn-relay-k8s/.sunbeam-build.yaml b/charms/ovn-relay-k8s/.sunbeam-build.yaml index 3216257a..b7d23719 100644 --- a/charms/ovn-relay-k8s/.sunbeam-build.yaml +++ b/charms/ovn-relay-k8s/.sunbeam-build.yaml @@ -2,5 +2,7 @@ external-libraries: - charms.tls_certificates_interface.v3.tls_certificates - charms.observability_libs.v1.kubernetes_service_patch - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.ovn_central_k8s.v0.ovsdb diff --git a/charms/ovn-relay-k8s/metadata.yaml b/charms/ovn-relay-k8s/metadata.yaml index 96e3f32f..01c84ca5 100644 --- a/charms/ovn-relay-k8s/metadata.yaml +++ b/charms/ovn-relay-k8s/metadata.yaml @@ -37,6 +37,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: ovsdb-cms-relay: diff --git a/charms/ovn-relay-k8s/src/charm.py b/charms/ovn-relay-k8s/src/charm.py index a6ca1447..d685e8ce 100755 --- a/charms/ovn-relay-k8s/src/charm.py +++ b/charms/ovn-relay-k8s/src/charm.py @@ -42,6 +42,7 @@ import ops_sunbeam.ovn.config_contexts as ovn_ctxts import ops_sunbeam.ovn.container_handlers as ovn_chandlers import ops_sunbeam.ovn.relation_handlers as ovn_relation_handlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from charms.observability_libs.v1.kubernetes_service_patch import ( KubernetesServicePatch, ) @@ -57,6 +58,7 @@ logger = logging.getLogger(__name__) OVSDB_SERVER = "ovsdb-server" +@sunbeam_tracing.trace_type class OVNRelayPebbleHandler(ovn_chandlers.OVNPebbleHandler): """Handler for OVN Relay container.""" @@ -89,6 +91,7 @@ class OVNRelayPebbleHandler(ovn_chandlers.OVNPebbleHandler): self.start_service() +@sunbeam_tracing.trace_sunbeam_charm class OVNRelayOperatorCharm(ovn_charm.OSBaseOVNOperatorCharm): """Charm the service.""" diff --git a/charms/placement-k8s/.sunbeam-build.yaml b/charms/placement-k8s/.sunbeam-build.yaml index aae7dbc3..24e0a900 100644 --- a/charms/placement-k8s/.sunbeam-build.yaml +++ b/charms/placement-k8s/.sunbeam-build.yaml @@ -4,6 +4,8 @@ external-libraries: - charms.traefik_k8s.v2.ingress - charms.certificate_transfer_interface.v0.certificate_transfer - charms.loki_k8s.v1.loki_push_api + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v1.identity_service templates: diff --git a/charms/placement-k8s/metadata.yaml b/charms/placement-k8s/metadata.yaml index 8000069d..1938d18f 100644 --- a/charms/placement-k8s/metadata.yaml +++ b/charms/placement-k8s/metadata.yaml @@ -46,6 +46,10 @@ requires: logging: interface: loki_push_api optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: placement: diff --git a/charms/placement-k8s/src/charm.py b/charms/placement-k8s/src/charm.py index f1d7763d..dc218712 100755 --- a/charms/placement-k8s/src/charm.py +++ b/charms/placement-k8s/src/charm.py @@ -29,6 +29,7 @@ import ops.pebble import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.tracing as sunbeam_tracing from ops.framework import ( StoredState, ) @@ -39,6 +40,7 @@ from ops.main import ( logger = logging.getLogger(__name__) +@sunbeam_tracing.trace_type class WSGIPlacementPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): """Placement Pebble Handler.""" @@ -59,6 +61,7 @@ class WSGIPlacementPebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): super().init_service(context) +@sunbeam_tracing.trace_sunbeam_charm class PlacementOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" diff --git a/charms/sunbeam-clusterd/.sunbeam-build.yaml b/charms/sunbeam-clusterd/.sunbeam-build.yaml index d582c071..b4661145 100644 --- a/charms/sunbeam-clusterd/.sunbeam-build.yaml +++ b/charms/sunbeam-clusterd/.sunbeam-build.yaml @@ -1,3 +1,5 @@ external-libraries: - charms.operator_libs_linux.v2.snap - charms.tls_certificates_interface.v3.tls_certificates + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing diff --git a/charms/sunbeam-clusterd/charmcraft.yaml b/charms/sunbeam-clusterd/charmcraft.yaml index 3f59e880..28fe91af 100644 --- a/charms/sunbeam-clusterd/charmcraft.yaml +++ b/charms/sunbeam-clusterd/charmcraft.yaml @@ -57,3 +57,7 @@ requires: certificates: interface: tls-certificates optional: True + tracing: + interface: tracing + optional: true + limit: 1 diff --git a/charms/sunbeam-clusterd/src/charm.py b/charms/sunbeam-clusterd/src/charm.py index cbb1d36c..040fdcdf 100755 --- a/charms/sunbeam-clusterd/src/charm.py +++ b/charms/sunbeam-clusterd/src/charm.py @@ -33,6 +33,7 @@ import ops.framework import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing import requests import tenacity from charms.operator_libs_linux.v2 import ( @@ -64,6 +65,7 @@ def _identity(x: bool) -> bool: return x +@sunbeam_tracing.trace_type class ClusterCertificatesHandler(sunbeam_rhandlers.TlsCertificatesHandler): """Handler for certificates interface.""" @@ -145,6 +147,7 @@ class ClusterCertificatesHandler(sunbeam_rhandlers.TlsCertificatesHandler): return {} +@sunbeam_tracing.trace_sunbeam_charm class SunbeamClusterdCharm(sunbeam_charm.OSBaseOperatorCharm): """Charm the service.""" diff --git a/charms/sunbeam-clusterd/src/relation_handlers.py b/charms/sunbeam-clusterd/src/relation_handlers.py index 335d8b1d..b9646058 100644 --- a/charms/sunbeam-clusterd/src/relation_handlers.py +++ b/charms/sunbeam-clusterd/src/relation_handlers.py @@ -23,6 +23,7 @@ import ops import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.interfaces as sunbeam_interfaces import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing logger = logging.getLogger(__name__) @@ -133,6 +134,7 @@ class ClusterdPeers(sunbeam_interfaces.OperatorPeers): ) +@sunbeam_tracing.trace_type class ClusterdPeerHandler(sunbeam_rhandlers.BasePeerHandler): """Base handler for managing a peers relation.""" @@ -151,7 +153,7 @@ class ClusterdPeerHandler(sunbeam_rhandlers.BasePeerHandler): def setup_event_handler(self) -> ops.Object: """Configure event handlers for peer relation.""" logger.debug("Setting up peer event handler") - peer_int = ClusterdPeers(self.charm, self.relation_name) # type: ignore + peer_int = sunbeam_tracing.trace_type(ClusterdPeers(self.charm, self.relation_name)) # type: ignore self.framework.observe(peer_int.on.add_node, self._on_add_node) self.framework.observe(peer_int.on.node_added, self._on_node_added) diff --git a/charms/sunbeam-machine/.sunbeam-build.yaml b/charms/sunbeam-machine/.sunbeam-build.yaml index b05a87fb..5acad28b 100644 --- a/charms/sunbeam-machine/.sunbeam-build.yaml +++ b/charms/sunbeam-machine/.sunbeam-build.yaml @@ -1,2 +1,4 @@ external-libraries: - charms.operator_libs_linux.v0.sysctl + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing diff --git a/charms/sunbeam-machine/metadata.yaml b/charms/sunbeam-machine/metadata.yaml index 445aed2e..6eceabdd 100644 --- a/charms/sunbeam-machine/metadata.yaml +++ b/charms/sunbeam-machine/metadata.yaml @@ -9,3 +9,9 @@ description: | # This charm has no peer relation by design. This charm needs to scale to # hundreds of units and this is limited by the peer relation. + +requires: + tracing: + interface: tracing + optional: true + limit: 1 diff --git a/charms/sunbeam-machine/src/charm.py b/charms/sunbeam-machine/src/charm.py index 8de402fc..78fb93fc 100755 --- a/charms/sunbeam-machine/src/charm.py +++ b/charms/sunbeam-machine/src/charm.py @@ -26,6 +26,7 @@ import logging import ops.framework import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.guard as sunbeam_guard +import ops_sunbeam.tracing as sunbeam_tracing from charms.operator_libs_linux.v0 import ( sysctl, ) @@ -37,6 +38,7 @@ ETC_ENVIRONMENT = "/etc/environment" logger = logging.getLogger(__name__) +@sunbeam_tracing.trace_sunbeam_charm class SunbeamMachineCharm(sunbeam_charm.OSBaseOperatorCharm): """Charm the service.""" diff --git a/charms/tempest-k8s/.sunbeam-build.yaml b/charms/tempest-k8s/.sunbeam-build.yaml index 9f4a96b0..ebba1f29 100644 --- a/charms/tempest-k8s/.sunbeam-build.yaml +++ b/charms/tempest-k8s/.sunbeam-build.yaml @@ -3,6 +3,8 @@ external-libraries: - charms.grafana_k8s.v0.grafana_dashboard - charms.loki_k8s.v1.loki_push_api - charms.certificate_transfer_interface.v0.certificate_transfer + - charms.tempo_k8s.v2.tracing + - charms.tempo_k8s.v1.charm_tracing internal-libraries: - charms.keystone_k8s.v0.identity_resource templates: diff --git a/charms/tempest-k8s/charmcraft.yaml b/charms/tempest-k8s/charmcraft.yaml index 2f9781e5..7261c700 100644 --- a/charms/tempest-k8s/charmcraft.yaml +++ b/charms/tempest-k8s/charmcraft.yaml @@ -65,6 +65,10 @@ requires: receive-ca-cert: interface: certificate_transfer optional: true + tracing: + interface: tracing + optional: true + limit: 1 provides: grafana-dashboard: diff --git a/charms/tempest-k8s/src/charm.py b/charms/tempest-k8s/src/charm.py index 0e54a053..afad325c 100755 --- a/charms/tempest-k8s/src/charm.py +++ b/charms/tempest-k8s/src/charm.py @@ -35,6 +35,7 @@ import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from handlers import ( GrafanaDashboardRelationHandler, LoggingRelationHandler, @@ -84,6 +85,7 @@ LOKI_RELATION_NAME = "logging" logger = logging.getLogger(__name__) +@sunbeam_tracing.trace_type class TempestConfigurationContext(ConfigContext): """Configuration context for tempest.""" @@ -98,6 +100,7 @@ class TempestConfigurationContext(ConfigContext): } +@sunbeam_tracing.trace_sunbeam_charm class TempestOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): """Charm the service.""" diff --git a/charms/tempest-k8s/src/handlers.py b/charms/tempest-k8s/src/handlers.py index f2c35f4b..348bd1b4 100644 --- a/charms/tempest-k8s/src/handlers.py +++ b/charms/tempest-k8s/src/handlers.py @@ -37,6 +37,7 @@ import ops.model import ops.pebble import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from utils.alert_rules import ( ALERT_RULES_PATH, ) @@ -69,6 +70,7 @@ def assert_ready(f): return wrapper +@sunbeam_tracing.trace_type class TempestPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Pebble handler for the container.""" @@ -301,6 +303,7 @@ class TempestPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): logger.warning("Clean-up failed") +@sunbeam_tracing.trace_type class TempestUserIdentityRelationHandler(sunbeam_rhandlers.RelationHandler): """Relation handler for identity ops.""" @@ -385,7 +388,7 @@ class TempestUserIdentityRelationHandler(sunbeam_rhandlers.RelationHandler): import charms.keystone_k8s.v0.identity_resource as id_ops logger.debug("Setting up Identity Resource event handler") - ops_svc = id_ops.IdentityResourceRequires( + ops_svc = sunbeam_tracing.trace_type(id_ops.IdentityResourceRequires)( self.charm, self.relation_name, ) @@ -649,13 +652,16 @@ class TempestUserIdentityRelationHandler(sunbeam_rhandlers.RelationHandler): self.callback_f(event) +@sunbeam_tracing.trace_type class GrafanaDashboardRelationHandler(sunbeam_rhandlers.RelationHandler): """Relation handler for grafana-dashboard relation.""" def setup_event_handler(self) -> ops.framework.Object: """Configure event handlers for the relation.""" logger.debug("Setting up Grafana Dashboards Provider event handler") - interface = grafana_dashboard.GrafanaDashboardProvider( + interface = sunbeam_tracing.trace_type( + grafana_dashboard.GrafanaDashboardProvider + )( self.charm, relation_name=self.relation_name, dashboards_path="src/grafana_dashboards", @@ -668,13 +674,14 @@ class GrafanaDashboardRelationHandler(sunbeam_rhandlers.RelationHandler): return True +@sunbeam_tracing.trace_type class LoggingRelationHandler(sunbeam_rhandlers.RelationHandler): """Relation handler for logging relation.""" def setup_event_handler(self) -> ops.framework.Object: """Configure event handlers for the relation.""" logger.debug("Setting up Logging Provider event handler") - interface = loki_push_api.LogProxyConsumer( + interface = sunbeam_tracing.trace_type(loki_push_api.LogProxyConsumer)( self.charm, recursive=True, relation_name=self.relation_name, diff --git a/libs/external/lib/charms/tempo_k8s/v1/charm_tracing.py b/libs/external/lib/charms/tempo_k8s/v1/charm_tracing.py new file mode 100644 index 00000000..913b20bb --- /dev/null +++ b/libs/external/lib/charms/tempo_k8s/v1/charm_tracing.py @@ -0,0 +1,702 @@ +#!/usr/bin/env python3 +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. + +"""This charm library contains utilities to instrument your Charm with opentelemetry tracing data collection. + +(yes! charm code, not workload code!) + +This means that, if your charm is related to, for example, COS' Tempo charm, you will be able to inspect +in real time from the Grafana dashboard the execution flow of your charm. + +# Quickstart +Fetch the following charm libs (and ensure the minimum version/revision numbers are satisfied): + + charmcraft fetch-lib charms.tempo_k8s.v2.tracing # >= 1.10 + charmcraft fetch-lib charms.tempo_k8s.v1.charm_tracing # >= 2.7 + +Then edit your charm code to include: + +```python +# import the necessary charm libs +from charms.tempo_k8s.v2.tracing import TracingEndpointRequirer, charm_tracing_config +from charms.tempo_k8s.v1.charm_tracing import charm_tracing + +# decorate your charm class with charm_tracing: +@charm_tracing( + # forward-declare the instance attributes that the instrumentor will look up to obtain the + # tempo endpoint and server certificate + tracing_endpoint="tracing_endpoint", + server_cert="server_cert" +) +class MyCharm(CharmBase): + _path_to_cert = "/path/to/cert.crt" + # path to cert file **in the charm container**. Its presence will be used to determine whether + # the charm is ready to use tls for encrypting charm traces. If your charm does not support tls, + # you can ignore this and pass None to charm_tracing_config. + # If you do support TLS, you'll need to make sure that the server cert is copied to this location + # and kept up to date so the instrumentor can use it. + + def __init__(self, ...): + ... + self.tracing = TracingEndpointRequirer(self, ...) + self.tracing_endpoint, self.server_cert = charm_tracing_config(self.tracing, self._path_to_cert) +``` + +# Detailed usage +To use this library, you need to do two things: +1) decorate your charm class with + +`@trace_charm(tracing_endpoint="my_tracing_endpoint")` + +2) add to your charm a "my_tracing_endpoint" (you can name this attribute whatever you like) +**property**, **method** or **instance attribute** that returns an otlp http/https endpoint url. +If you are using the ``charms.tempo_k8s.v2.tracing.TracingEndpointRequirer`` as +``self.tracing = TracingEndpointRequirer(self)``, the implementation could be: + +``` + @property + def my_tracing_endpoint(self) -> Optional[str]: + '''Tempo endpoint for charm tracing''' + if self.tracing.is_ready(): + return self.tracing.get_endpoint("otlp_http") + else: + return None +``` + +At this point your charm will be automatically instrumented so that: +- charm execution starts a trace, containing + - every event as a span (including custom events) + - every charm method call (except dunders) as a span + + +## TLS support +If your charm integrates with a TLS provider which is also trusted by the tracing provider (the Tempo charm), +you can configure ``charm_tracing`` to use TLS by passing a ``server_cert`` parameter to the decorator. + +If your charm is not trusting the same CA as the Tempo endpoint it is sending traces to, +you'll need to implement a cert-transfer relation to obtain the CA certificate from the same +CA that Tempo is using. + +For example: +``` +from charms.tempo_k8s.v1.charm_tracing import trace_charm +@trace_charm( + tracing_endpoint="my_tracing_endpoint", + server_cert="_server_cert" +) +class MyCharm(CharmBase): + self._server_cert = "/path/to/server.crt" + ... + + def on_tls_changed(self, e) -> Optional[str]: + # update the server cert on the charm container for charm tracing + Path(self._server_cert).write_text(self.get_server_cert()) + + def on_tls_broken(self, e) -> Optional[str]: + # remove the server cert so charm_tracing won't try to use tls anymore + Path(self._server_cert).unlink() +``` + + +## More fine-grained manual instrumentation +if you wish to add more spans to the trace, you can do so by getting a hold of the tracer like so: +``` +import opentelemetry +... +def get_tracer(self) -> opentelemetry.trace.Tracer: + return opentelemetry.trace.get_tracer(type(self).__name__) +``` + +By default, the tracer is named after the charm type. If you wish to override that, you can pass +a different ``service_name`` argument to ``trace_charm``. + +See the official opentelemetry Python SDK documentation for usage: +https://opentelemetry-python.readthedocs.io/en/latest/ + +## Upgrading from `v0` + +If you are upgrading from `charm_tracing` v0, you need to take the following steps (assuming you already +have the newest version of the library in your charm): +1) If you need the dependency for your tests, add the following dependency to your charm project +(or, if your project had a dependency on `opentelemetry-exporter-otlp-proto-grpc` only because +of `charm_tracing` v0, you can replace it with): + +`opentelemetry-exporter-otlp-proto-http>=1.21.0`. + +2) Update the charm method referenced to from ``@trace`` and ``@trace_charm``, +to return from ``TracingEndpointRequirer.get_endpoint("otlp_http")`` instead of ``grpc_http``. +For example: + +``` + from charms.tempo_k8s.v0.charm_tracing import trace_charm + + @trace_charm( + tracing_endpoint="my_tracing_endpoint", + ) + class MyCharm(CharmBase): + + ... + + @property + def my_tracing_endpoint(self) -> Optional[str]: + '''Tempo endpoint for charm tracing''' + if self.tracing.is_ready(): + return self.tracing.otlp_grpc_endpoint() # OLD API, DEPRECATED. + else: + return None +``` + +needs to be replaced with: + +``` + from charms.tempo_k8s.v1.charm_tracing import trace_charm + + @trace_charm( + tracing_endpoint="my_tracing_endpoint", + ) + class MyCharm(CharmBase): + + ... + + @property + def my_tracing_endpoint(self) -> Optional[str]: + '''Tempo endpoint for charm tracing''' + if self.tracing.is_ready(): + return self.tracing.get_endpoint("otlp_http") # NEW API, use this. + else: + return None +``` + +3) If you were passing a certificate (str) using `server_cert`, you need to change it to +provide an *absolute* path to the certificate file instead. +""" + +import functools +import inspect +import logging +import os +import shutil +from contextlib import contextmanager +from contextvars import Context, ContextVar, copy_context +from importlib.metadata import distributions +from pathlib import Path +from typing import ( + Any, + Callable, + Generator, + Optional, + Sequence, + Type, + TypeVar, + Union, + cast, +) + +import opentelemetry +import ops +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import Span, TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.trace import INVALID_SPAN, Tracer +from opentelemetry.trace import get_current_span as otlp_get_current_span +from opentelemetry.trace import ( + get_tracer, + get_tracer_provider, + set_span_in_context, + set_tracer_provider, +) +from ops.charm import CharmBase +from ops.framework import Framework + +# The unique Charmhub library identifier, never change it +LIBID = "cb1705dcd1a14ca09b2e60187d1215c7" + +# Increment this major API version when introducing breaking changes +LIBAPI = 1 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version + +LIBPATCH = 12 + +PYDEPS = ["opentelemetry-exporter-otlp-proto-http==1.21.0"] + +logger = logging.getLogger("tracing") +dev_logger = logging.getLogger("tracing-dev") + +# set this to 0 if you are debugging/developing this library source +dev_logger.setLevel(logging.CRITICAL) + + +_CharmType = Type[CharmBase] # the type CharmBase and any subclass thereof +_C = TypeVar("_C", bound=_CharmType) +_T = TypeVar("_T", bound=type) +_F = TypeVar("_F", bound=Type[Callable]) +tracer: ContextVar[Tracer] = ContextVar("tracer") +_GetterType = Union[Callable[[_CharmType], Optional[str]], property] + +CHARM_TRACING_ENABLED = "CHARM_TRACING_ENABLED" + + +def is_enabled() -> bool: + """Whether charm tracing is enabled.""" + return os.getenv(CHARM_TRACING_ENABLED, "1") == "1" + + +@contextmanager +def charm_tracing_disabled(): + """Contextmanager to temporarily disable charm tracing. + + For usage in tests. + """ + previous = os.getenv(CHARM_TRACING_ENABLED, "1") + os.environ[CHARM_TRACING_ENABLED] = "0" + yield + os.environ[CHARM_TRACING_ENABLED] = previous + + +def get_current_span() -> Union[Span, None]: + """Return the currently active Span, if there is one, else None. + + If you'd rather keep your logic unconditional, you can use opentelemetry.trace.get_current_span, + which will return an object that behaves like a span but records no data. + """ + span = otlp_get_current_span() + if span is INVALID_SPAN: + return None + return cast(Span, span) + + +def _get_tracer_from_context(ctx: Context) -> Optional[ContextVar]: + tracers = [v for v in ctx if v is not None and v.name == "tracer"] + if tracers: + return tracers[0] + return None + + +def _get_tracer() -> Optional[Tracer]: + """Find tracer in context variable and as a fallback locate it in the full context.""" + try: + return tracer.get() + except LookupError: + try: + ctx: Context = copy_context() + if context_tracer := _get_tracer_from_context(ctx): + return context_tracer.get() + else: + return None + except LookupError: + return None + + +@contextmanager +def _span(name: str) -> Generator[Optional[Span], Any, Any]: + """Context to create a span if there is a tracer, otherwise do nothing.""" + if tracer := _get_tracer(): + with tracer.start_as_current_span(name) as span: + yield cast(Span, span) + else: + yield None + + +class TracingError(RuntimeError): + """Base class for errors raised by this module.""" + + +class UntraceableObjectError(TracingError): + """Raised when an object you're attempting to instrument cannot be autoinstrumented.""" + + +class TLSError(TracingError): + """Raised when the tracing endpoint is https but we don't have a cert yet.""" + + +def _get_tracing_endpoint( + tracing_endpoint_attr: str, + charm_instance: object, + charm_type: type, +): + _tracing_endpoint = getattr(charm_instance, tracing_endpoint_attr) + if callable(_tracing_endpoint): + tracing_endpoint = _tracing_endpoint() + else: + tracing_endpoint = _tracing_endpoint + + if tracing_endpoint is None: + return + + elif not isinstance(tracing_endpoint, str): + raise TypeError( + f"{charm_type.__name__}.{tracing_endpoint_attr} should resolve to a tempo endpoint (string); " + f"got {tracing_endpoint} instead." + ) + + dev_logger.debug(f"Setting up span exporter to endpoint: {tracing_endpoint}/v1/traces") + return f"{tracing_endpoint}/v1/traces" + + +def _get_server_cert( + server_cert_attr: str, + charm_instance: ops.CharmBase, + charm_type: Type[ops.CharmBase], +): + _server_cert = getattr(charm_instance, server_cert_attr) + if callable(_server_cert): + server_cert = _server_cert() + else: + server_cert = _server_cert + + if server_cert is None: + logger.warning( + f"{charm_type}.{server_cert_attr} is None; sending traces over INSECURE connection." + ) + return + elif not Path(server_cert).is_absolute(): + raise ValueError( + f"{charm_type}.{server_cert_attr} should resolve to a valid tls cert absolute path (string | Path)); " + f"got {server_cert} instead." + ) + return server_cert + + +def _remove_stale_otel_sdk_packages(): + """Hack to remove stale opentelemetry sdk packages from the charm's python venv. + + See https://github.com/canonical/grafana-agent-operator/issues/146 and + https://bugs.launchpad.net/juju/+bug/2058335 for more context. This patch can be removed after + this juju issue is resolved and sufficient time has passed to expect most users of this library + have migrated to the patched version of juju. + + This only does something if executed on an upgrade-charm event. + """ + if os.getenv("JUJU_DISPATCH_PATH") == "hooks/upgrade-charm": + logger.debug("Executing _remove_stale_otel_sdk_packages patch on charm upgrade") + # Find any opentelemetry_sdk distributions + otel_sdk_distributions = list(distributions(name="opentelemetry_sdk")) + # If there is more than 1, inspect each and if it has 0 entrypoints, infer that it is stale + if len(otel_sdk_distributions) > 1: + for distribution in otel_sdk_distributions: + if len(distribution.entry_points) == 0: + # Distribution appears to be empty. Remove it + path = distribution._path # type: ignore + logger.debug(f"Removing empty opentelemetry_sdk distribution at: {path}") + shutil.rmtree(path) + + +def _setup_root_span_initializer( + charm_type: _CharmType, + tracing_endpoint_attr: str, + server_cert_attr: Optional[str], + service_name: Optional[str] = None, +): + """Patch the charm's initializer.""" + original_init = charm_type.__init__ + + @functools.wraps(original_init) + def wrap_init(self: CharmBase, framework: Framework, *args, **kwargs): + # we're using 'self' here because this is charm init code, makes sense to read what's below + # from the perspective of the charm. Self.unit.name... + + original_init(self, framework, *args, **kwargs) + # we call this from inside the init context instead of, say, _autoinstrument, because we want it to + # be checked on a per-charm-instantiation basis, not on a per-type-declaration one. + if not is_enabled(): + # this will only happen during unittesting, hopefully, so it's fine to log a + # bit more verbosely + logger.info("Tracing DISABLED: skipping root span initialization") + return + + # already init some attrs that will be reinited later by calling original_init: + # self.framework = framework + # self.handle = Handle(None, self.handle_kind, None) + + original_event_context = framework._event_context + # default service name isn't just app name because it could conflict with the workload service name + _service_name = service_name or f"{self.app.name}-charm" + + unit_name = self.unit.name + # apply hacky patch to remove stale opentelemetry sdk packages on upgrade-charm. + # it could be trouble if someone ever decides to implement their own tracer parallel to + # ours and before the charm has inited. We assume they won't. + _remove_stale_otel_sdk_packages() + resource = Resource.create( + attributes={ + "service.name": _service_name, + "compose_service": _service_name, + "charm_type": type(self).__name__, + # juju topology + "juju_unit": unit_name, + "juju_application": self.app.name, + "juju_model": self.model.name, + "juju_model_uuid": self.model.uuid, + } + ) + provider = TracerProvider(resource=resource) + + # if anything goes wrong with retrieving the endpoint, we let the exception bubble up. + tracing_endpoint = _get_tracing_endpoint(tracing_endpoint_attr, self, charm_type) + + if not tracing_endpoint: + # tracing is off if tracing_endpoint is None + return + + server_cert: Optional[Union[str, Path]] = ( + _get_server_cert(server_cert_attr, self, charm_type) if server_cert_attr else None + ) + + if tracing_endpoint.startswith("https://") and not server_cert: + raise TLSError( + "Tracing endpoint is https, but no server_cert has been passed." + "Please point @trace_charm to a `server_cert` attr." + ) + + exporter = OTLPSpanExporter( + endpoint=tracing_endpoint, + certificate_file=str(Path(server_cert).absolute()) if server_cert else None, + timeout=2, + ) + + processor = BatchSpanProcessor(exporter) + provider.add_span_processor(processor) + set_tracer_provider(provider) + _tracer = get_tracer(_service_name) # type: ignore + _tracer_token = tracer.set(_tracer) + + dispatch_path = os.getenv("JUJU_DISPATCH_PATH", "") # something like hooks/install + event_name = dispatch_path.split("/")[1] if "/" in dispatch_path else dispatch_path + root_span_name = f"{unit_name}: {event_name} event" + span = _tracer.start_span(root_span_name, attributes={"juju.dispatch_path": dispatch_path}) + + # all these shenanigans are to work around the fact that the opentelemetry tracing API is built + # on the assumption that spans will be used as contextmanagers. + # Since we don't (as we need to close the span on framework.commit), + # we need to manually set the root span as current. + ctx = set_span_in_context(span) + + # log a trace id, so we can pick it up from the logs (and jhack) to look it up in tempo. + root_trace_id = hex(span.get_span_context().trace_id)[2:] # strip 0x prefix + logger.debug(f"Starting root trace with id={root_trace_id!r}.") + + span_token = opentelemetry.context.attach(ctx) # type: ignore + + @contextmanager + def wrap_event_context(event_name: str): + dev_logger.info(f"entering event context: {event_name}") + # when the framework enters an event context, we create a span. + with _span("event: " + event_name) as event_context_span: + if event_context_span: + # todo: figure out how to inject event attrs in here + event_context_span.add_event(event_name) + yield original_event_context(event_name) + + framework._event_context = wrap_event_context # type: ignore + + original_close = framework.close + + @functools.wraps(original_close) + def wrap_close(): + dev_logger.info("tearing down tracer and flushing traces") + span.end() + opentelemetry.context.detach(span_token) # type: ignore + tracer.reset(_tracer_token) + tp = cast(TracerProvider, get_tracer_provider()) + tp.force_flush(timeout_millis=1000) # don't block for too long + tp.shutdown() + original_close() + + framework.close = wrap_close + return + + charm_type.__init__ = wrap_init # type: ignore + + +def trace_charm( + tracing_endpoint: str, + server_cert: Optional[str] = None, + service_name: Optional[str] = None, + extra_types: Sequence[type] = (), +) -> Callable[[_T], _T]: + """Autoinstrument the decorated charm with tracing telemetry. + + Use this function to get out-of-the-box traces for all events emitted on this charm and all + method calls on instances of this class. + + Usage: + >>> from charms.tempo_k8s.v1.charm_tracing import trace_charm + >>> from charms.tempo_k8s.v1.tracing import TracingEndpointRequirer + >>> from ops import CharmBase + >>> + >>> @trace_charm( + >>> tracing_endpoint="tempo_otlp_http_endpoint", + >>> ) + >>> class MyCharm(CharmBase): + >>> + >>> def __init__(self, framework: Framework): + >>> ... + >>> self.tracing = TracingEndpointRequirer(self) + >>> + >>> @property + >>> def tempo_otlp_http_endpoint(self) -> Optional[str]: + >>> if self.tracing.is_ready(): + >>> return self.tracing.otlp_http_endpoint() + >>> else: + >>> return None + >>> + + :param tracing_endpoint: name of a method, property or attribute on the charm type that returns an + optional (fully resolvable) tempo url to which the charm traces will be pushed. + If None, tracing will be effectively disabled. + :param server_cert: name of a method, property or attribute on the charm type that returns an + optional absolute path to a CA certificate file to be used when sending traces to a remote server. + If it returns None, an _insecure_ connection will be used. To avoid errors in transient + situations where the endpoint is already https but there is no certificate on disk yet, it + is recommended to disable tracing (by returning None from the tracing_endpoint) altogether + until the cert has been written to disk. + :param service_name: service name tag to attach to all traces generated by this charm. + Defaults to the juju application name this charm is deployed under. + :param extra_types: pass any number of types that you also wish to autoinstrument. + For example, charm libs, relation endpoint wrappers, workload abstractions, ... + """ + + def _decorator(charm_type: _T) -> _T: + """Autoinstrument the wrapped charmbase type.""" + _autoinstrument( + charm_type, + tracing_endpoint_attr=tracing_endpoint, + server_cert_attr=server_cert, + service_name=service_name, + extra_types=extra_types, + ) + return charm_type + + return _decorator + + +def _autoinstrument( + charm_type: _T, + tracing_endpoint_attr: str, + server_cert_attr: Optional[str] = None, + service_name: Optional[str] = None, + extra_types: Sequence[type] = (), +) -> _T: + """Set up tracing on this charm class. + + Use this function to get out-of-the-box traces for all events emitted on this charm and all + method calls on instances of this class. + + Usage: + + >>> from charms.tempo_k8s.v1.charm_tracing import _autoinstrument + >>> from ops.main import main + >>> _autoinstrument( + >>> MyCharm, + >>> tracing_endpoint_attr="tempo_otlp_http_endpoint", + >>> service_name="MyCharm", + >>> extra_types=(Foo, Bar) + >>> ) + >>> main(MyCharm) + + :param charm_type: the CharmBase subclass to autoinstrument. + :param tracing_endpoint_attr: name of a method, property or attribute on the charm type that returns an + optional (fully resolvable) tempo url to which the charm traces will be pushed. + If None, tracing will be effectively disabled. + :param server_cert_attr: name of a method, property or attribute on the charm type that returns an + optional absolute path to a CA certificate file to be used when sending traces to a remote server. + If it returns None, an _insecure_ connection will be used. To avoid errors in transient + situations where the endpoint is already https but there is no certificate on disk yet, it + is recommended to disable tracing (by returning None from the tracing_endpoint) altogether + until the cert has been written to disk. + :param service_name: service name tag to attach to all traces generated by this charm. + Defaults to the juju application name this charm is deployed under. + :param extra_types: pass any number of types that you also wish to autoinstrument. + For example, charm libs, relation endpoint wrappers, workload abstractions, ... + """ + dev_logger.info(f"instrumenting {charm_type}") + _setup_root_span_initializer( + charm_type, + tracing_endpoint_attr, + server_cert_attr=server_cert_attr, + service_name=service_name, + ) + trace_type(charm_type) + for type_ in extra_types: + trace_type(type_) + + return charm_type + + +def trace_type(cls: _T) -> _T: + """Set up tracing on this class. + + Use this decorator to get out-of-the-box traces for all method calls on instances of this class. + It assumes that this class is only instantiated after a charm type decorated with `@trace_charm` + has been instantiated. + """ + dev_logger.info(f"instrumenting {cls}") + for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): + dev_logger.info(f"discovered {method}") + + if method.__name__.startswith("__"): + dev_logger.info(f"skipping {method} (dunder)") + continue + + new_method = trace_method(method) + if isinstance(inspect.getattr_static(cls, method.__name__), staticmethod): + new_method = staticmethod(new_method) + setattr(cls, name, new_method) + + return cls + + +def trace_method(method: _F) -> _F: + """Trace this method. + + A span will be opened when this method is called and closed when it returns. + """ + return _trace_callable(method, "method") + + +def trace_function(function: _F) -> _F: + """Trace this function. + + A span will be opened when this function is called and closed when it returns. + """ + return _trace_callable(function, "function") + + +def _trace_callable(callable: _F, qualifier: str) -> _F: + dev_logger.info(f"instrumenting {callable}") + + # sig = inspect.signature(callable) + @functools.wraps(callable) + def wrapped_function(*args, **kwargs): # type: ignore + name = getattr(callable, "__qualname__", getattr(callable, "__name__", str(callable))) + with _span(f"{qualifier} call: {name}"): # type: ignore + return callable(*args, **kwargs) # type: ignore + + # wrapped_function.__signature__ = sig + return wrapped_function # type: ignore + + +def trace(obj: Union[Type, Callable]): + """Trace this object and send the resulting spans to Tempo. + + It will dispatch to ``trace_type`` if the decorated object is a class, otherwise + ``trace_function``. + """ + if isinstance(obj, type): + if issubclass(obj, CharmBase): + raise ValueError( + "cannot use @trace on CharmBase subclasses: use @trace_charm instead " + "(we need some arguments!)" + ) + return trace_type(obj) + else: + try: + return trace_function(obj) + except Exception: + raise UntraceableObjectError( + f"cannot create span from {type(obj)}; instrument {obj} manually." + ) diff --git a/libs/external/lib/charms/tempo_k8s/v2/tracing.py b/libs/external/lib/charms/tempo_k8s/v2/tracing.py new file mode 100644 index 00000000..dfb23365 --- /dev/null +++ b/libs/external/lib/charms/tempo_k8s/v2/tracing.py @@ -0,0 +1,987 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +"""## Overview. + +This document explains how to integrate with the Tempo charm for the purpose of pushing traces to a +tracing endpoint provided by Tempo. It also explains how alternative implementations of the Tempo charm +may maintain the same interface and be backward compatible with all currently integrated charms. + +## Requirer Library Usage + +Charms seeking to push traces to Tempo, must do so using the `TracingEndpointRequirer` +object from this charm library. For the simplest use cases, using the `TracingEndpointRequirer` +object only requires instantiating it, typically in the constructor of your charm. The +`TracingEndpointRequirer` constructor requires the name of the relation over which a tracing endpoint + is exposed by the Tempo charm, and a list of protocols it intends to send traces with. + This relation must use the `tracing` interface. + The `TracingEndpointRequirer` object may be instantiated as follows + + from charms.tempo_k8s.v2.tracing import TracingEndpointRequirer + + def __init__(self, *args): + super().__init__(*args) + # ... + self.tracing = TracingEndpointRequirer(self, + protocols=['otlp_grpc', 'otlp_http', 'jaeger_http_thrift'] + ) + # ... + +Note that the first argument (`self`) to `TracingEndpointRequirer` is always a reference to the +parent charm. + +Alternatively to providing the list of requested protocols at init time, the charm can do it at +any point in time by calling the +`TracingEndpointRequirer.request_protocols(*protocol:str, relation:Optional[Relation])` method. +Using this method also allows you to use per-relation protocols. + +Units of provider charms obtain the tempo endpoint to which they will push their traces by calling +`TracingEndpointRequirer.get_endpoint(protocol: str)`, where `protocol` is, for example: +- `otlp_grpc` +- `otlp_http` +- `zipkin` +- `tempo` + +If the `protocol` is not in the list of protocols that the charm requested at endpoint set-up time, +the library will raise an error. + +## Requirer Library Usage + +The `TracingEndpointProvider` object may be used by charms to manage relations with their +trace sources. For this purposes a Tempo-like charm needs to do two things + +1. Instantiate the `TracingEndpointProvider` object by providing it a +reference to the parent (Tempo) charm and optionally the name of the relation that the Tempo charm +uses to interact with its trace sources. This relation must conform to the `tracing` interface +and it is strongly recommended that this relation be named `tracing` which is its +default value. + +For example a Tempo charm may instantiate the `TracingEndpointProvider` in its constructor as +follows + + from charms.tempo_k8s.v2.tracing import TracingEndpointProvider + + def __init__(self, *args): + super().__init__(*args) + # ... + self.tracing = TracingEndpointProvider(self) + # ... + + + +""" # noqa: W505 +import enum +import json +import logging +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Literal, + MutableMapping, + Optional, + Sequence, + Tuple, + Union, + cast, +) + +import pydantic +from ops.charm import ( + CharmBase, + CharmEvents, + RelationBrokenEvent, + RelationEvent, + RelationRole, +) +from ops.framework import EventSource, Object +from ops.model import ModelError, Relation +from pydantic import BaseModel, ConfigDict, Field + +# The unique Charmhub library identifier, never change it +LIBID = "12977e9aa0b34367903d8afeb8c3d85d" + +# Increment this major API version when introducing breaking changes +LIBAPI = 2 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 8 + +PYDEPS = ["pydantic"] + +logger = logging.getLogger(__name__) + +DEFAULT_RELATION_NAME = "tracing" +RELATION_INTERFACE_NAME = "tracing" + +# Supported list rationale https://github.com/canonical/tempo-coordinator-k8s-operator/issues/8 +ReceiverProtocol = Literal[ + "zipkin", + "otlp_grpc", + "otlp_http", + "jaeger_grpc", + "jaeger_thrift_http", +] + +RawReceiver = Tuple[ReceiverProtocol, str] +"""Helper type. A raw receiver is defined as a tuple consisting of the protocol name, and the (external, if available), +(secured, if available) resolvable server url. +""" + +BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} + + +class TransportProtocolType(str, enum.Enum): + """Receiver Type.""" + + http = "http" + grpc = "grpc" + + +receiver_protocol_to_transport_protocol: Dict[ReceiverProtocol, TransportProtocolType] = { + "zipkin": TransportProtocolType.http, + "otlp_grpc": TransportProtocolType.grpc, + "otlp_http": TransportProtocolType.http, + "jaeger_thrift_http": TransportProtocolType.http, + "jaeger_grpc": TransportProtocolType.grpc, +} +"""A mapping between telemetry protocols and their corresponding transport protocol. +""" + + +class TracingError(Exception): + """Base class for custom errors raised by this library.""" + + +class NotReadyError(TracingError): + """Raised by the provider wrapper if a requirer hasn't published the required data (yet).""" + + +class ProtocolNotRequestedError(TracingError): + """Raised if the user attempts to obtain an endpoint for a protocol it did not request.""" + + +class DataValidationError(TracingError): + """Raised when data validation fails on IPU relation data.""" + + +class AmbiguousRelationUsageError(TracingError): + """Raised when one wrongly assumes that there can only be one relation on an endpoint.""" + + +if int(pydantic.version.VERSION.split(".")[0]) < 2: + + class DatabagModel(BaseModel): # type: ignore + """Base databag model.""" + + class Config: + """Pydantic config.""" + + # ignore any extra fields in the databag + extra = "ignore" + """Ignore any extra fields in the databag.""" + allow_population_by_field_name = True + """Allow instantiating this class by field name (instead of forcing alias).""" + + _NEST_UNDER = None + + @classmethod + def load(cls, databag: MutableMapping): + """Load this model from a Juju databag.""" + if cls._NEST_UNDER: + return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) + + try: + data = { + k: json.loads(v) + for k, v in databag.items() + # Don't attempt to parse model-external values + if k in {f.alias for f in cls.__fields__.values()} + } + except json.JSONDecodeError as e: + msg = f"invalid databag contents: expecting json. {databag}" + logger.error(msg) + raise DataValidationError(msg) from e + + try: + return cls.parse_raw(json.dumps(data)) # type: ignore + except pydantic.ValidationError as e: + msg = f"failed to validate databag: {databag}" + logger.debug(msg, exc_info=True) + raise DataValidationError(msg) from e + + def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): + """Write the contents of this model to Juju databag. + + :param databag: the databag to write the data to. + :param clear: ensure the databag is cleared before writing it. + """ + if clear and databag: + databag.clear() + + if databag is None: + databag = {} + + if self._NEST_UNDER: + databag[self._NEST_UNDER] = self.json(by_alias=True) + return databag + + dct = self.dict() + for key, field in self.__fields__.items(): # type: ignore + value = dct[key] + databag[field.alias or key] = json.dumps(value) + + return databag + +else: + from pydantic import ConfigDict + + class DatabagModel(BaseModel): + """Base databag model.""" + + model_config = ConfigDict( + # ignore any extra fields in the databag + extra="ignore", + # Allow instantiating this class by field name (instead of forcing alias). + populate_by_name=True, + # Custom config key: whether to nest the whole datastructure (as json) + # under a field or spread it out at the toplevel. + _NEST_UNDER=None, # type: ignore + ) + """Pydantic config.""" + + @classmethod + def load(cls, databag: MutableMapping): + """Load this model from a Juju databag.""" + nest_under = cls.model_config.get("_NEST_UNDER") # type: ignore + if nest_under: + return cls.model_validate(json.loads(databag[nest_under])) # type: ignore + + try: + data = { + k: json.loads(v) + for k, v in databag.items() + # Don't attempt to parse model-external values + if k in {(f.alias or n) for n, f in cls.__fields__.items()} + } + except json.JSONDecodeError as e: + msg = f"invalid databag contents: expecting json. {databag}" + logger.error(msg) + raise DataValidationError(msg) from e + + try: + return cls.model_validate_json(json.dumps(data)) # type: ignore + except pydantic.ValidationError as e: + msg = f"failed to validate databag: {databag}" + logger.debug(msg, exc_info=True) + raise DataValidationError(msg) from e + + def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): + """Write the contents of this model to Juju databag. + + :param databag: the databag to write the data to. + :param clear: ensure the databag is cleared before writing it. + """ + if clear and databag: + databag.clear() + + if databag is None: + databag = {} + nest_under = self.model_config.get("_NEST_UNDER") + if nest_under: + databag[nest_under] = self.model_dump_json( # type: ignore + by_alias=True, + # skip keys whose values are default + exclude_defaults=True, + ) + return databag + + dct = self.model_dump() # type: ignore + for key, field in self.model_fields.items(): # type: ignore + value = dct[key] + if value == field.default: + continue + databag[field.alias or key] = json.dumps(value) + + return databag + + +# todo use models from charm-relation-interfaces +if int(pydantic.version.VERSION.split(".")[0]) < 2: + + class ProtocolType(BaseModel): # type: ignore + """Protocol Type.""" + + class Config: + """Pydantic config.""" + + use_enum_values = True + """Allow serializing enum values.""" + + name: str = Field( + ..., + description="Receiver protocol name. What protocols are supported (and what they are called) " + "may differ per provider.", + examples=["otlp_grpc", "otlp_http", "tempo_http"], + ) + + type: TransportProtocolType = Field( + ..., + description="The transport protocol used by this receiver.", + examples=["http", "grpc"], + ) + +else: + + class ProtocolType(BaseModel): + """Protocol Type.""" + + model_config = ConfigDict( + # Allow serializing enum values. + use_enum_values=True + ) + """Pydantic config.""" + + name: str = Field( + ..., + description="Receiver protocol name. What protocols are supported (and what they are called) " + "may differ per provider.", + examples=["otlp_grpc", "otlp_http", "tempo_http"], + ) + + type: TransportProtocolType = Field( + ..., + description="The transport protocol used by this receiver.", + examples=["http", "grpc"], + ) + + +class Receiver(BaseModel): + """Specification of an active receiver.""" + + protocol: ProtocolType = Field(..., description="Receiver protocol name and type.") + url: str = Field( + ..., + description="""URL at which the receiver is reachable. If there's an ingress, it would be the external URL. + Otherwise, it would be the service's fqdn or internal IP. + If the protocol type is grpc, the url will not contain a scheme.""", + examples=[ + "http://traefik_address:2331", + "https://traefik_address:2331", + "http://tempo_public_ip:2331", + "https://tempo_public_ip:2331", + "tempo_public_ip:2331", + ], + ) + + +class TracingProviderAppData(DatabagModel): # noqa: D101 + """Application databag model for the tracing provider.""" + + receivers: List[Receiver] = Field( + ..., + description="List of all receivers enabled on the tracing provider.", + ) + + +class TracingRequirerAppData(DatabagModel): # noqa: D101 + """Application databag model for the tracing requirer.""" + + receivers: List[ReceiverProtocol] + """Requested receivers.""" + + +class _AutoSnapshotEvent(RelationEvent): + __args__: Tuple[str, ...] = () + __optional_kwargs__: Dict[str, Any] = {} + + @classmethod + def __attrs__(cls): + return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) + + def __init__(self, handle, relation, *args, **kwargs): + super().__init__(handle, relation) + + if not len(self.__args__) == len(args): + raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) + + for attr, obj in zip(self.__args__, args): + setattr(self, attr, obj) + for attr, default in self.__optional_kwargs__.items(): + obj = kwargs.get(attr, default) + setattr(self, attr, obj) + + def snapshot(self) -> dict: + dct = super().snapshot() + for attr in self.__attrs__(): + obj = getattr(self, attr) + try: + dct[attr] = obj + except ValueError as e: + raise ValueError( + "cannot automagically serialize {}: " + "override this method and do it " + "manually.".format(obj) + ) from e + + return dct + + def restore(self, snapshot: dict) -> None: + super().restore(snapshot) + for attr, obj in snapshot.items(): + setattr(self, attr, obj) + + +class RelationNotFoundError(Exception): + """Raised if no relation with the given name is found.""" + + def __init__(self, relation_name: str): + self.relation_name = relation_name + self.message = "No relation named '{}' found".format(relation_name) + super().__init__(self.message) + + +class RelationInterfaceMismatchError(Exception): + """Raised if the relation with the given name has an unexpected interface.""" + + def __init__( + self, + relation_name: str, + expected_relation_interface: str, + actual_relation_interface: str, + ): + self.relation_name = relation_name + self.expected_relation_interface = expected_relation_interface + self.actual_relation_interface = actual_relation_interface + self.message = ( + "The '{}' relation has '{}' as interface rather than the expected '{}'".format( + relation_name, actual_relation_interface, expected_relation_interface + ) + ) + + super().__init__(self.message) + + +class RelationRoleMismatchError(Exception): + """Raised if the relation with the given name has a different role than expected.""" + + def __init__( + self, + relation_name: str, + expected_relation_role: RelationRole, + actual_relation_role: RelationRole, + ): + self.relation_name = relation_name + self.expected_relation_interface = expected_relation_role + self.actual_relation_role = actual_relation_role + self.message = "The '{}' relation has role '{}' rather than the expected '{}'".format( + relation_name, repr(actual_relation_role), repr(expected_relation_role) + ) + + super().__init__(self.message) + + +def _validate_relation_by_interface_and_direction( + charm: CharmBase, + relation_name: str, + expected_relation_interface: str, + expected_relation_role: RelationRole, +): + """Validate a relation. + + Verifies that the `relation_name` provided: (1) exists in metadata.yaml, + (2) declares as interface the interface name passed as `relation_interface` + and (3) has the right "direction", i.e., it is a relation that `charm` + provides or requires. + + Args: + charm: a `CharmBase` object to scan for the matching relation. + relation_name: the name of the relation to be verified. + expected_relation_interface: the interface name to be matched by the + relation named `relation_name`. + expected_relation_role: whether the `relation_name` must be either + provided or required by `charm`. + + Raises: + RelationNotFoundError: If there is no relation in the charm's metadata.yaml + with the same name as provided via `relation_name` argument. + RelationInterfaceMismatchError: The relation with the same name as provided + via `relation_name` argument does not have the same relation interface + as specified via the `expected_relation_interface` argument. + RelationRoleMismatchError: If the relation with the same name as provided + via `relation_name` argument does not have the same role as specified + via the `expected_relation_role` argument. + """ + if relation_name not in charm.meta.relations: + raise RelationNotFoundError(relation_name) + + relation = charm.meta.relations[relation_name] + + # fixme: why do we need to cast here? + actual_relation_interface = cast(str, relation.interface_name) + + if actual_relation_interface != expected_relation_interface: + raise RelationInterfaceMismatchError( + relation_name, expected_relation_interface, actual_relation_interface + ) + + if expected_relation_role is RelationRole.provides: + if relation_name not in charm.meta.provides: + raise RelationRoleMismatchError( + relation_name, RelationRole.provides, RelationRole.requires + ) + elif expected_relation_role is RelationRole.requires: + if relation_name not in charm.meta.requires: + raise RelationRoleMismatchError( + relation_name, RelationRole.requires, RelationRole.provides + ) + else: + raise TypeError("Unexpected RelationDirection: {}".format(expected_relation_role)) + + +class RequestEvent(RelationEvent): + """Event emitted when a remote requests a tracing endpoint.""" + + @property + def requested_receivers(self) -> List[ReceiverProtocol]: + """List of receiver protocols that have been requested.""" + relation = self.relation + app = relation.app + if not app: + raise NotReadyError("relation.app is None") + + return TracingRequirerAppData.load(relation.data[app]).receivers + + +class BrokenEvent(RelationBrokenEvent): + """Event emitted when a relation on tracing is broken.""" + + +class TracingEndpointProviderEvents(CharmEvents): + """TracingEndpointProvider events.""" + + request = EventSource(RequestEvent) + broken = EventSource(BrokenEvent) + + +class TracingEndpointProvider(Object): + """Class representing a trace receiver service.""" + + on = TracingEndpointProviderEvents() # type: ignore + + def __init__( + self, + charm: CharmBase, + external_url: Optional[str] = None, + relation_name: str = DEFAULT_RELATION_NAME, + ): + """Initialize. + + Args: + charm: a `CharmBase` instance that manages this instance of the Tempo service. + external_url: external address of the node hosting the tempo server, + if an ingress is present. + relation_name: an optional string name of the relation between `charm` + and the Tempo charmed service. The default is "tracing". + + Raises: + RelationNotFoundError: If there is no relation in the charm's metadata.yaml + with the same name as provided via `relation_name` argument. + RelationInterfaceMismatchError: The relation with the same name as provided + via `relation_name` argument does not have the `tracing` relation + interface. + RelationRoleMismatchError: If the relation with the same name as provided + via `relation_name` argument does not have the `RelationRole.requires` + role. + """ + _validate_relation_by_interface_and_direction( + charm, relation_name, RELATION_INTERFACE_NAME, RelationRole.provides + ) + + super().__init__(charm, relation_name + "tracing-provider") + self._charm = charm + self._external_url = external_url + self._relation_name = relation_name + self.framework.observe( + self._charm.on[relation_name].relation_joined, self._on_relation_event + ) + self.framework.observe( + self._charm.on[relation_name].relation_created, self._on_relation_event + ) + self.framework.observe( + self._charm.on[relation_name].relation_changed, self._on_relation_event + ) + self.framework.observe( + self._charm.on[relation_name].relation_broken, self._on_relation_broken_event + ) + + def _on_relation_broken_event(self, e: RelationBrokenEvent): + """Handle relation broken events.""" + self.on.broken.emit(e.relation) + + def _on_relation_event(self, e: RelationEvent): + """Handle relation created/joined/changed events.""" + if self.is_requirer_ready(e.relation): + self.on.request.emit(e.relation) + + def is_requirer_ready(self, relation: Relation): + """Attempt to determine if requirer has already populated app data.""" + try: + self._get_requested_protocols(relation) + except NotReadyError: + return False + return True + + @staticmethod + def _get_requested_protocols(relation: Relation): + app = relation.app + if not app: + raise NotReadyError("relation.app is None") + + try: + databag = TracingRequirerAppData.load(relation.data[app]) + except (json.JSONDecodeError, pydantic.ValidationError, DataValidationError): + logger.info(f"relation {relation} is not ready to talk tracing") + raise NotReadyError() + return databag.receivers + + def requested_protocols(self): + """All receiver protocols that have been requested by our related apps.""" + requested_protocols = set() + for relation in self.relations: + try: + protocols = self._get_requested_protocols(relation) + except NotReadyError: + continue + requested_protocols.update(protocols) + return requested_protocols + + @property + def relations(self) -> List[Relation]: + """All relations active on this endpoint.""" + return self._charm.model.relations[self._relation_name] + + def publish_receivers(self, receivers: Sequence[RawReceiver]): + """Let all requirers know that these receivers are active and listening.""" + if not self._charm.unit.is_leader(): + raise RuntimeError("only leader can do this") + + for relation in self.relations: + try: + TracingProviderAppData( + receivers=[ + Receiver( + url=url, + protocol=ProtocolType( + name=protocol, + type=receiver_protocol_to_transport_protocol[protocol], + ), + ) + for protocol, url in receivers + ], + ).dump(relation.data[self._charm.app]) + + except ModelError as e: + # args are bytes + msg = e.args[0] + if isinstance(msg, bytes): + if msg.startswith( + b"ERROR cannot read relation application settings: permission denied" + ): + logger.error( + f"encountered error {e} while attempting to update_relation_data." + f"The relation must be gone." + ) + continue + raise + + +class EndpointRemovedEvent(RelationBrokenEvent): + """Event representing a change in one of the receiver endpoints.""" + + +class EndpointChangedEvent(_AutoSnapshotEvent): + """Event representing a change in one of the receiver endpoints.""" + + __args__ = ("_receivers",) + + if TYPE_CHECKING: + _receivers = [] # type: List[dict] + + @property + def receivers(self) -> List[Receiver]: + """Cast receivers back from dict.""" + return [Receiver(**i) for i in self._receivers] + + +class TracingEndpointRequirerEvents(CharmEvents): + """TracingEndpointRequirer events.""" + + endpoint_changed = EventSource(EndpointChangedEvent) + endpoint_removed = EventSource(EndpointRemovedEvent) + + +class TracingEndpointRequirer(Object): + """A tracing endpoint for Tempo.""" + + on = TracingEndpointRequirerEvents() # type: ignore + + def __init__( + self, + charm: CharmBase, + relation_name: str = DEFAULT_RELATION_NAME, + protocols: Optional[List[ReceiverProtocol]] = None, + ): + """Construct a tracing requirer for a Tempo charm. + + If your application supports pushing traces to a distributed tracing backend, the + `TracingEndpointRequirer` object enables your charm to easily access endpoint information + exchanged over a `tracing` relation interface. + + Args: + charm: a `CharmBase` object that manages this + `TracingEndpointRequirer` object. Typically, this is `self` in the instantiating + class. + relation_name: an optional string name of the relation between `charm` + and the Tempo charmed service. The default is "tracing". It is strongly + advised not to change the default, so that people deploying your charm will have a + consistent experience with all other charms that provide tracing endpoints. + protocols: optional list of protocols that the charm intends to send traces with. + The provider will enable receivers for these and only these protocols, + so be sure to enable all protocols the charm or its workload are going to need. + + Raises: + RelationNotFoundError: If there is no relation in the charm's metadata.yaml + with the same name as provided via `relation_name` argument. + RelationInterfaceMismatchError: The relation with the same name as provided + via `relation_name` argument does not have the `tracing` relation + interface. + RelationRoleMismatchError: If the relation with the same name as provided + via `relation_name` argument does not have the `RelationRole.provides` + role. + """ + _validate_relation_by_interface_and_direction( + charm, relation_name, RELATION_INTERFACE_NAME, RelationRole.requires + ) + + super().__init__(charm, relation_name) + + self._is_single_endpoint = charm.meta.relations[relation_name].limit == 1 + + self._charm = charm + self._relation_name = relation_name + + events = self._charm.on[self._relation_name] + self.framework.observe(events.relation_changed, self._on_tracing_relation_changed) + self.framework.observe(events.relation_broken, self._on_tracing_relation_broken) + + if protocols: + self.request_protocols(protocols) + + def request_protocols( + self, protocols: Sequence[ReceiverProtocol], relation: Optional[Relation] = None + ): + """Publish the list of protocols which the provider should activate.""" + # todo: should we check if _is_single_endpoint and len(self.relations) > 1 and raise, here? + relations = [relation] if relation else self.relations + + if not protocols: + # empty sequence + raise ValueError( + "You need to pass a nonempty sequence of protocols to `request_protocols`." + ) + + try: + if self._charm.unit.is_leader(): + for relation in relations: + TracingRequirerAppData( + receivers=list(protocols), + ).dump(relation.data[self._charm.app]) + + except ModelError as e: + # args are bytes + msg = e.args[0] + if isinstance(msg, bytes): + if msg.startswith( + b"ERROR cannot read relation application settings: permission denied" + ): + logger.error( + f"encountered error {e} while attempting to request_protocols." + f"The relation must be gone." + ) + return + raise + + @property + def relations(self) -> List[Relation]: + """The tracing relations associated with this endpoint.""" + return self._charm.model.relations[self._relation_name] + + @property + def _relation(self) -> Optional[Relation]: + """If this wraps a single endpoint, the relation bound to it, if any.""" + if not self._is_single_endpoint: + objname = type(self).__name__ + raise AmbiguousRelationUsageError( + f"This {objname} wraps a {self._relation_name} endpoint that has " + "limit != 1. We can't determine what relation, of the possibly many, you are " + f"talking about. Please pass a relation instance while calling {objname}, " + "or set limit=1 in the charm metadata." + ) + relations = self.relations + return relations[0] if relations else None + + def is_ready(self, relation: Optional[Relation] = None): + """Is this endpoint ready?""" + relation = relation or self._relation + if not relation: + logger.debug(f"no relation on {self._relation_name !r}: tracing not ready") + return False + if relation.data is None: + logger.error(f"relation data is None for {relation}") + return False + if not relation.app: + logger.error(f"{relation} event received but there is no relation.app") + return False + try: + databag = dict(relation.data[relation.app]) + TracingProviderAppData.load(databag) + + except (json.JSONDecodeError, pydantic.ValidationError, DataValidationError): + logger.info(f"failed validating relation data for {relation}") + return False + return True + + def _on_tracing_relation_changed(self, event): + """Notify the providers that there is new endpoint information available.""" + relation = event.relation + if not self.is_ready(relation): + self.on.endpoint_removed.emit(relation) # type: ignore + return + + data = TracingProviderAppData.load(relation.data[relation.app]) + self.on.endpoint_changed.emit(relation, [i.dict() for i in data.receivers]) # type: ignore + + def _on_tracing_relation_broken(self, event: RelationBrokenEvent): + """Notify the providers that the endpoint is broken.""" + relation = event.relation + self.on.endpoint_removed.emit(relation) # type: ignore + + def get_all_endpoints( + self, relation: Optional[Relation] = None + ) -> Optional[TracingProviderAppData]: + """Unmarshalled relation data.""" + relation = relation or self._relation + if not self.is_ready(relation): + return + return TracingProviderAppData.load(relation.data[relation.app]) # type: ignore + + def _get_endpoint( + self, relation: Optional[Relation], protocol: ReceiverProtocol + ) -> Optional[str]: + app_data = self.get_all_endpoints(relation) + if not app_data: + return None + receivers: List[Receiver] = list( + filter(lambda i: i.protocol.name == protocol, app_data.receivers) + ) + if not receivers: + logger.error(f"no receiver found with protocol={protocol!r}") + return + if len(receivers) > 1: + logger.error( + f"too many receivers with protocol={protocol!r}; using first one. Found: {receivers}" + ) + return + + receiver = receivers[0] + return receiver.url + + def get_endpoint( + self, protocol: ReceiverProtocol, relation: Optional[Relation] = None + ) -> Optional[str]: + """Receiver endpoint for the given protocol.""" + endpoint = self._get_endpoint(relation or self._relation, protocol=protocol) + if not endpoint: + requested_protocols = set() + relations = [relation] if relation else self.relations + for relation in relations: + try: + databag = TracingRequirerAppData.load(relation.data[self._charm.app]) + except DataValidationError: + continue + + requested_protocols.update(databag.receivers) + + if protocol not in requested_protocols: + raise ProtocolNotRequestedError(protocol, relation) + + return None + return endpoint + + +def charm_tracing_config( + endpoint_requirer: TracingEndpointRequirer, cert_path: Optional[Union[Path, str]] +) -> Tuple[Optional[str], Optional[str]]: + """Utility function to determine the charm_tracing config you will likely want. + + If no endpoint is provided: + disable charm tracing. + If https endpoint is provided but cert_path is not found on disk: + disable charm tracing. + If https endpoint is provided and cert_path is None: + ERROR + Else: + proceed with charm tracing (with or without tls, as appropriate) + + Usage: + If you are using charm_tracing >= v1.9: + >>> from lib.charms.tempo_k8s.v1.charm_tracing import trace_charm + >>> from lib.charms.tempo_k8s.v2.tracing import charm_tracing_config + >>> @trace_charm(tracing_endpoint="my_endpoint", cert_path="cert_path") + >>> class MyCharm(...): + >>> _cert_path = "/path/to/cert/on/charm/container.crt" + >>> def __init__(self, ...): + >>> self.tracing = TracingEndpointRequirer(...) + >>> self.my_endpoint, self.cert_path = charm_tracing_config( + ... self.tracing, self._cert_path) + + If you are using charm_tracing < v1.9: + >>> from lib.charms.tempo_k8s.v1.charm_tracing import trace_charm + >>> from lib.charms.tempo_k8s.v2.tracing import charm_tracing_config + >>> @trace_charm(tracing_endpoint="my_endpoint", cert_path="cert_path") + >>> class MyCharm(...): + >>> _cert_path = "/path/to/cert/on/charm/container.crt" + >>> def __init__(self, ...): + >>> self.tracing = TracingEndpointRequirer(...) + >>> self._my_endpoint, self._cert_path = charm_tracing_config( + ... self.tracing, self._cert_path) + >>> @property + >>> def my_endpoint(self): + >>> return self._my_endpoint + >>> @property + >>> def cert_path(self): + >>> return self._cert_path + + """ + if not endpoint_requirer.is_ready(): + return None, None + + endpoint = endpoint_requirer.get_endpoint("otlp_http") + if not endpoint: + return None, None + + is_https = endpoint.startswith("https://") + + if is_https: + if cert_path is None: + raise TracingError("Cannot send traces to an https endpoint without a certificate.") + elif not Path(cert_path).exists(): + # if endpoint is https BUT we don't have a server_cert yet: + # disable charm tracing until we do to prevent tls errors + return None, None + return endpoint, str(cert_path) + else: + return endpoint, None diff --git a/ops-sunbeam/ops_sunbeam/charm.py b/ops-sunbeam/ops_sunbeam/charm.py index 362b1657..c00d6285 100644 --- a/ops-sunbeam/ops_sunbeam/charm.py +++ b/ops-sunbeam/ops_sunbeam/charm.py @@ -138,6 +138,10 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): ) -> List[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = handlers or [] + if self.can_add_handler("tracing", handlers): + self.tracing = sunbeam_rhandlers.TracingRequireHandler( + self, "tracing", "tracing" in self.mandatory_relations + ) if self.can_add_handler("amqp", handlers): self.amqp = sunbeam_rhandlers.RabbitMQHandler( self, @@ -201,6 +205,12 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): return handlers + def get_tracing_endpoint(self) -> str | None: + """Get the tracing endpoint for the service.""" + if hasattr(self, "tracing"): + return self.tracing.tracing_endpoint() + return None + def get_sans_ips(self) -> List[str]: """Return Subject Alternate Names to use in cert for service.""" str_ips_sans = [str(s) for s in self._ip_sans()] diff --git a/ops-sunbeam/ops_sunbeam/compound_status.py b/ops-sunbeam/ops_sunbeam/compound_status.py index 841f3dbe..f7d0624f 100644 --- a/ops-sunbeam/ops_sunbeam/compound_status.py +++ b/ops-sunbeam/ops_sunbeam/compound_status.py @@ -31,6 +31,7 @@ from typing import ( Tuple, ) +import ops_sunbeam.tracing as sunbeam_tracing from ops.charm import ( CharmBase, ) @@ -123,6 +124,7 @@ class Status: } +@sunbeam_tracing.trace_type class StatusPool(Object): """A pool of Status objects. diff --git a/ops-sunbeam/ops_sunbeam/config_contexts.py b/ops-sunbeam/ops_sunbeam/config_contexts.py index 1ccdf263..655e1bf3 100644 --- a/ops-sunbeam/ops_sunbeam/config_contexts.py +++ b/ops-sunbeam/ops_sunbeam/config_contexts.py @@ -28,6 +28,8 @@ from typing import ( TYPE_CHECKING, ) +import ops_sunbeam.tracing as sunbeam_tracing + if TYPE_CHECKING: import ops_sunbeam.charm @@ -38,6 +40,7 @@ ERASURE_CODED = "erasure-coded" REPLICATED = "replicated" +@sunbeam_tracing.trace_type class ConfigContext: """Base class used for creating a config context.""" @@ -63,6 +66,7 @@ class ConfigContext: raise NotImplementedError +@sunbeam_tracing.trace_type class CharmConfigContext(ConfigContext): """A context containing all of the charms config options.""" @@ -71,6 +75,7 @@ class CharmConfigContext(ConfigContext): return self.charm.config +@sunbeam_tracing.trace_type class WSGIWorkerConfigContext(ConfigContext): """Configuration context for WSGI configuration.""" @@ -88,6 +93,7 @@ class WSGIWorkerConfigContext(ConfigContext): } +@sunbeam_tracing.trace_type class CephConfigurationContext(ConfigContext): """Ceph configuration context.""" @@ -103,6 +109,7 @@ class CephConfigurationContext(ConfigContext): return ctxt +@sunbeam_tracing.trace_type class CinderCephConfigurationContext(ConfigContext): """Cinder Ceph configuration context.""" diff --git a/ops-sunbeam/ops_sunbeam/container_handlers.py b/ops-sunbeam/ops_sunbeam/container_handlers.py index a47a6249..340e7319 100644 --- a/ops-sunbeam/ops_sunbeam/container_handlers.py +++ b/ops-sunbeam/ops_sunbeam/container_handlers.py @@ -34,6 +34,7 @@ import ops.pebble import ops_sunbeam.compound_status as compound_status import ops_sunbeam.core as sunbeam_core import ops_sunbeam.templating as sunbeam_templating +import ops_sunbeam.tracing as sunbeam_tracing from ops.model import ( ActiveStatus, BlockedStatus, @@ -47,6 +48,7 @@ ContainerDir = collections.namedtuple( ) +@sunbeam_tracing.trace_type class PebbleHandler(ops.framework.Object): """Base handler for Pebble based containers.""" @@ -319,6 +321,7 @@ class PebbleHandler(ops.framework.Object): """Called when files have changed before restarting services.""" +@sunbeam_tracing.trace_type class ServicePebbleHandler(PebbleHandler): """Container handler for containers which manage a service.""" @@ -356,6 +359,7 @@ class ServicePebbleHandler(PebbleHandler): self.start_all(restart=restart) +@sunbeam_tracing.trace_type class WSGIPebbleHandler(PebbleHandler): """WSGI oriented handler for a Pebble managed container.""" diff --git a/ops-sunbeam/ops_sunbeam/core.py b/ops-sunbeam/ops_sunbeam/core.py index f0b66744..d242ac54 100644 --- a/ops-sunbeam/ops_sunbeam/core.py +++ b/ops-sunbeam/ops_sunbeam/core.py @@ -23,6 +23,8 @@ from typing import ( Union, ) +import ops_sunbeam.tracing as sunbeam_tracing + if TYPE_CHECKING: from ops_sunbeam.charm import ( OSBaseOperatorCharm, @@ -41,6 +43,7 @@ ContainerConfigFile = collections.namedtuple( ) +@sunbeam_tracing.trace_type class OPSCharmContexts: """Set of config contexts and contexts from relation handlers.""" diff --git a/ops-sunbeam/ops_sunbeam/ovn/config_contexts.py b/ops-sunbeam/ops_sunbeam/ovn/config_contexts.py index bcd175bc..553518ac 100644 --- a/ops-sunbeam/ops_sunbeam/ovn/config_contexts.py +++ b/ops-sunbeam/ops_sunbeam/ovn/config_contexts.py @@ -20,8 +20,10 @@ These are not specific to a relation. """ from .. import config_contexts as sunbeam_ccontexts +from .. import tracing as sunbeam_tracing +@sunbeam_tracing.trace_type class OVNDBConfigContext(sunbeam_ccontexts.ConfigContext): """Context for OVN charms.""" diff --git a/ops-sunbeam/ops_sunbeam/ovn/container_handlers.py b/ops-sunbeam/ops_sunbeam/ovn/container_handlers.py index 24d9a62b..00f92421 100644 --- a/ops-sunbeam/ops_sunbeam/ovn/container_handlers.py +++ b/ops-sunbeam/ops_sunbeam/ovn/container_handlers.py @@ -24,8 +24,10 @@ from ops.model import ( from .. import container_handlers as sunbeam_chandlers from .. import core as sunbeam_core +from .. import tracing as sunbeam_tracing +@sunbeam_tracing.trace_type class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Common class for OVN services.""" diff --git a/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py b/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py index b9028116..409b9e34 100644 --- a/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py +++ b/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py @@ -32,10 +32,12 @@ from ops.model import ( ) from .. import relation_handlers as sunbeam_rhandlers +from .. import tracing as sunbeam_tracing logger = logging.getLogger(__name__) +@sunbeam_tracing.trace_type class OVNRelationUtils: """Common utilities for processing OVN relations.""" @@ -306,6 +308,7 @@ class OVNRelationUtils: return list(set(addresses)) +@sunbeam_tracing.trace_type class OVNDBClusterPeerHandler( sunbeam_rhandlers.BasePeerHandler, OVNRelationUtils ): @@ -458,6 +461,7 @@ class OVNDBClusterPeerHandler( return ctxt +@sunbeam_tracing.trace_type class OVSDBCMSProvidesHandler( sunbeam_rhandlers.RelationHandler, OVNRelationUtils ): @@ -481,7 +485,7 @@ class OVSDBCMSProvidesHandler( logger.debug("Setting up ovs-cms provides event handler") import charms.ovn_central_k8s.v0.ovsdb as ovsdb - ovsdb_svc = ovsdb.OVSDBCMSProvides( + ovsdb_svc = sunbeam_tracing.trace_type(ovsdb.OVSDBCMSProvides)( self.charm, self.relation_name, ) @@ -510,6 +514,7 @@ class OVSDBCMSProvidesHandler( return True +@sunbeam_tracing.trace_type class OVSDBCMSRequiresHandler( sunbeam_rhandlers.RelationHandler, OVNRelationUtils ): @@ -532,7 +537,7 @@ class OVSDBCMSRequiresHandler( logger.debug("Setting up ovs-cms requires event handler") import charms.ovn_central_k8s.v0.ovsdb as ovsdb - ovsdb_svc = ovsdb.OVSDBCMSRequires( + ovsdb_svc = sunbeam_tracing.trace_type(ovsdb.OVSDBCMSRequires)( self.charm, self.relation_name, ) diff --git a/ops-sunbeam/ops_sunbeam/relation_handlers.py b/ops-sunbeam/ops_sunbeam/relation_handlers.py index 866bd9e4..e54c5de7 100644 --- a/ops-sunbeam/ops_sunbeam/relation_handlers.py +++ b/ops-sunbeam/ops_sunbeam/relation_handlers.py @@ -38,6 +38,7 @@ import ops.charm import ops.framework import ops_sunbeam.compound_status as compound_status import ops_sunbeam.interfaces as sunbeam_interfaces +import ops_sunbeam.tracing as sunbeam_tracing from ops.model import ( ActiveStatus, BlockedStatus, @@ -53,6 +54,7 @@ ERASURE_CODED = "erasure-coded" REPLICATED = "replicated" +@sunbeam_tracing.trace_type class RelationHandler(ops.framework.Object): """Base handler class for relations. @@ -151,6 +153,7 @@ class RelationHandler(ops.framework.Object): raise NotImplementedError +@sunbeam_tracing.trace_type class IngressHandler(RelationHandler): """Base class to handle Ingress relations.""" @@ -175,7 +178,7 @@ class IngressHandler(RelationHandler): IngressPerAppRequirer, ) - interface = IngressPerAppRequirer( + interface = sunbeam_tracing.trace_type(IngressPerAppRequirer)( self.charm, self.relation_name, port=self.default_ingress_port, @@ -247,14 +250,17 @@ class IngressHandler(RelationHandler): } +@sunbeam_tracing.trace_type class IngressInternalHandler(IngressHandler): """Handler for Ingress relations on internal interface.""" +@sunbeam_tracing.trace_type class IngressPublicHandler(IngressHandler): """Handler for Ingress relations on public interface.""" +@sunbeam_tracing.trace_type class DBHandler(RelationHandler): """Handler for DB relations.""" @@ -285,6 +291,10 @@ class DBHandler(RelationHandler): # from trigger handlers for other dbs. # It also must be a valid python identifier. alias = self.relation_name.replace("-", "_") + # tracing this library is currently failing + # implement when either one of these is fixed: + # https://github.com/canonical/tempo-k8s-operator/issues/155 + # https://github.com/canonical/data-platform-libs/issues/186 db = DatabaseRequires( self.charm, self.relation_name, @@ -386,6 +396,7 @@ class DBHandler(RelationHandler): } +@sunbeam_tracing.trace_type class RabbitMQHandler(RelationHandler): """Handler for managing a rabbitmq relation.""" @@ -412,7 +423,7 @@ class RabbitMQHandler(RelationHandler): # has this relation. import charms.rabbitmq_k8s.v0.rabbitmq as sunbeam_rabbitmq - amqp = sunbeam_rabbitmq.RabbitMQRequires( + amqp = sunbeam_tracing.trace_type(sunbeam_rabbitmq.RabbitMQRequires)( self.charm, self.relation_name, self.username, self.vhost ) self.framework.observe(amqp.on.ready, self._on_amqp_ready) @@ -473,12 +484,14 @@ class RabbitMQHandler(RelationHandler): return ctxt +@sunbeam_tracing.trace_type class AMQPHandler(RabbitMQHandler): """Backwards compatibility class for older library consumers.""" pass +@sunbeam_tracing.trace_type class IdentityServiceRequiresHandler(RelationHandler): """Handler for managing a identity-service relation.""" @@ -501,7 +514,7 @@ class IdentityServiceRequiresHandler(RelationHandler): logger.debug("Setting up Identity Service event handler") import charms.keystone_k8s.v1.identity_service as sun_id - id_svc = sun_id.IdentityServiceRequires( + id_svc = sunbeam_tracing.trace_type(sun_id.IdentityServiceRequires)( self.charm, self.relation_name, self.service_endpoints, self.region ) self.framework.observe( @@ -544,6 +557,7 @@ class IdentityServiceRequiresHandler(RelationHandler): return False +@sunbeam_tracing.trace_type class BasePeerHandler(RelationHandler): """Base handler for managing a peers relation.""" @@ -554,7 +568,9 @@ class BasePeerHandler(RelationHandler): logger.debug("Setting up peer event handler") # Lazy import to ensure this lib is only required if the charm # has this relation. - peer_int = sunbeam_interfaces.OperatorPeers( + peer_int = sunbeam_tracing.trace_type( + sunbeam_interfaces.OperatorPeers + )( self.charm, self.relation_name, ) @@ -639,6 +655,7 @@ class BasePeerHandler(RelationHandler): ) +@sunbeam_tracing.trace_type class CephClientHandler(RelationHandler): """Handler for ceph-client interface.""" @@ -663,7 +680,7 @@ class CephClientHandler(RelationHandler): # has this relation. import interface_ceph_client.ceph_client as ceph_client - ceph = ceph_client.CephClientRequires( + ceph = sunbeam_tracing.trace_type(ceph_client.CephClientRequires)( self.charm, self.relation_name, ) @@ -857,6 +874,7 @@ class _Store(abc.ABC): self.save_entry(name, entry) +@sunbeam_tracing.trace_type class TlsCertificatesHandler(RelationHandler): """Handler for certificates interface.""" @@ -1008,9 +1026,9 @@ class TlsCertificatesHandler(RelationHandler): TLSCertificatesRequiresV3, ) - self.certificates = TLSCertificatesRequiresV3( - self.charm, "certificates" - ) + self.certificates = sunbeam_tracing.trace_type( + TLSCertificatesRequiresV3 + )(self.charm, "certificates") self.framework.observe( self.charm.on.certificates_relation_joined, @@ -1247,6 +1265,7 @@ class TlsCertificatesHandler(RelationHandler): return ctxt +@sunbeam_tracing.trace_type class IdentityCredentialsRequiresHandler(RelationHandler): """Handles the identity credentials relation on the requires side.""" @@ -1277,7 +1296,9 @@ class IdentityCredentialsRequiresHandler(RelationHandler): import charms.keystone_k8s.v0.identity_credentials as identity_credentials logger.debug("Setting up the identity-credentials event handler") - credentials_service = identity_credentials.IdentityCredentialsRequires( + credentials_service = sunbeam_tracing.trace_type( + identity_credentials.IdentityCredentialsRequires + )( self.charm, self.relation_name, ) @@ -1308,6 +1329,7 @@ class IdentityCredentialsRequiresHandler(RelationHandler): return False +@sunbeam_tracing.trace_type class IdentityResourceRequiresHandler(RelationHandler): """Handles the identity resource relation on the requires side.""" @@ -1341,7 +1363,7 @@ class IdentityResourceRequiresHandler(RelationHandler): import charms.keystone_k8s.v0.identity_resource as ops_svc logger.debug("Setting up Identity Resource event handler") - ops_svc = ops_svc.IdentityResourceRequires( + ops_svc = sunbeam_tracing.trace_type(ops_svc.IdentityResourceRequires)( self.charm, self.relation_name, ) @@ -1384,6 +1406,7 @@ class IdentityResourceRequiresHandler(RelationHandler): return self.interface.ready() +@sunbeam_tracing.trace_type class CeilometerServiceRequiresHandler(RelationHandler): """Handle ceilometer service relation on the requires side.""" @@ -1417,7 +1440,9 @@ class CeilometerServiceRequiresHandler(RelationHandler): import charms.ceilometer_k8s.v0.ceilometer_service as ceilometer_svc logger.debug("Setting up Ceilometer service event handler") - svc = ceilometer_svc.CeilometerServiceRequires( + svc = sunbeam_tracing.trace_type( + ceilometer_svc.CeilometerServiceRequires + )( self.charm, self.relation_name, ) @@ -1454,6 +1479,7 @@ class CeilometerServiceRequiresHandler(RelationHandler): return False +@sunbeam_tracing.trace_type class CephAccessRequiresHandler(RelationHandler): """Handles the ceph access relation on the requires side.""" @@ -1484,7 +1510,9 @@ class CephAccessRequiresHandler(RelationHandler): import charms.cinder_ceph_k8s.v0.ceph_access as ceph_access logger.debug("Setting up the ceph-access event handler") - ceph_access = ceph_access.CephAccessRequires( + ceph_access = sunbeam_tracing.trace_type( + ceph_access.CephAccessRequires + )( self.charm, self.relation_name, ) @@ -1524,6 +1552,7 @@ class CephAccessRequiresHandler(RelationHandler): ExtraOpsProcess = Callable[[ops.EventBase, dict], None] +@sunbeam_tracing.trace_type class UserIdentityResourceRequiresHandler(RelationHandler): """Handle user management on IdentityResource relation.""" @@ -1585,7 +1614,7 @@ class UserIdentityResourceRequiresHandler(RelationHandler): import charms.keystone_k8s.v0.identity_resource as id_ops logger.debug("Setting up Identity Resource event handler") - ops_svc = id_ops.IdentityResourceRequires( + ops_svc = sunbeam_tracing.trace_type(id_ops.IdentityResourceRequires)( self.charm, self.relation_name, ) @@ -1958,6 +1987,7 @@ class UserIdentityResourceRequiresHandler(RelationHandler): return self.get_config_credentials() is not None +@sunbeam_tracing.trace_type class CertificateTransferRequiresHandler(RelationHandler): """Handle certificate transfer relation on the requires side.""" @@ -1994,7 +2024,7 @@ class CertificateTransferRequiresHandler(RelationHandler): CertificateTransferRequires, ) - recv_ca_cert = CertificateTransferRequires( + recv_ca_cert = sunbeam_tracing.trace_type(CertificateTransferRequires)( self.charm, "receive-ca-cert" ) self.framework.observe( @@ -2039,6 +2069,7 @@ class CertificateTransferRequiresHandler(RelationHandler): return {"ca_bundle": "\n".join(ca_bundle)} +@sunbeam_tracing.trace_type class TraefikRouteHandler(RelationHandler): """Base class to handle traefik route relations.""" @@ -2061,7 +2092,7 @@ class TraefikRouteHandler(RelationHandler): TraefikRouteRequirer, ) - interface = TraefikRouteRequirer( + interface = sunbeam_tracing.trace_type(TraefikRouteRequirer)( self.charm, self.model.get_relation(self.relation_name), self.relation_name, @@ -2112,6 +2143,7 @@ class TraefikRouteHandler(RelationHandler): } +@sunbeam_tracing.trace_type class NovaServiceRequiresHandler(RelationHandler): """Handle nova service relation on the requires side.""" @@ -2145,7 +2177,7 @@ class NovaServiceRequiresHandler(RelationHandler): import charms.nova_k8s.v0.nova_service as nova_svc logger.debug("Setting up Nova service event handler") - svc = nova_svc.NovaServiceRequires( + svc = sunbeam_tracing.trace_type(nova_svc.NovaServiceRequires)( self.charm, self.relation_name, ) @@ -2180,6 +2212,7 @@ class NovaServiceRequiresHandler(RelationHandler): return False +@sunbeam_tracing.trace_type class LogForwardHandler(RelationHandler): """Handle log forward relation on the requires side.""" @@ -2210,7 +2243,7 @@ class LogForwardHandler(RelationHandler): import charms.loki_k8s.v1.loki_push_api as loki_push_api logger.debug("Setting up log forward event handler") - log_forwarder = loki_push_api.LogForwarder( + log_forwarder = sunbeam_tracing.trace_type(loki_push_api.LogForwarder)( self.charm, relation_name=self.relation_name, ) @@ -2220,3 +2253,54 @@ class LogForwardHandler(RelationHandler): def ready(self) -> bool: """Whether handler is ready for use.""" return self.interface.is_ready() + + +@sunbeam_tracing.trace_type +class TracingRequireHandler(RelationHandler): + """Handle tracing relation on the requires side.""" + + def __init__( + self, + charm: ops.charm.CharmBase, + relation_name: str, + mandatory: bool = False, + protocols: list[str] | None = None, + ) -> None: + """Create a new tracing-relation handler. + + :param charm: the Charm class the handler + :type charm: ops.charm.CharmBase + :param relation_name: the relation the handler is bound to + :type relation_name: str + :param mandatory: If the relation is mandatory to proceed with + configuring charm. + :type mandatory: bool + """ + if protocols is None: + protocols = ["otlp_http"] + self.protocols = protocols + super().__init__(charm, relation_name, lambda *args: None, mandatory) + + def setup_event_handler(self) -> ops.Object: + """Configure event handlers for tracing relation.""" + import charms.tempo_k8s.v2.tracing as tracing + + tracing_interface = sunbeam_tracing.trace_type( + tracing.TracingEndpointRequirer + )( + self.charm, + self.relation_name, + protocols=self.protocols, # type: ignore[arg-type] + ) + + return tracing_interface + + def tracing_endpoint(self) -> str | None: + """Otlp endpoint for charm tracing.""" + if self.ready(): + return self.interface.get_endpoint("otlp_http") + return None + + def ready(self) -> bool: + """Whether handler is ready for use.""" + return self.interface.is_ready() diff --git a/ops-sunbeam/ops_sunbeam/tracing.py b/ops-sunbeam/ops_sunbeam/tracing.py new file mode 100644 index 00000000..3cf653e1 --- /dev/null +++ b/ops-sunbeam/ops_sunbeam/tracing.py @@ -0,0 +1,130 @@ +# Copyright 2024 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Utilities for tracing.""" + +from typing import ( + Any, + Callable, + Optional, + Sequence, + TypeVar, + overload, +) + +_T = TypeVar("_T") + +try: + from charms.tempo_k8s.v1.charm_tracing import ( + trace_type, + ) +except ImportError: + + def trace_type(cls: _T) -> _T: + """No-op decorator for tracing.""" + return cls + + +try: + from charms.tempo_k8s.v1.charm_tracing import ( + trace_charm, + ) +except ImportError: + + def trace_charm( + tracing_endpoint: str, + server_cert: Optional[str] = None, + service_name: Optional[str] = None, + extra_types: Sequence[type] = (), + ) -> Callable[[_T], _T]: + """No-op decorator for tracing.""" + + def _wrapper(charm_cls: _T) -> _T: + return charm_cls + + return _wrapper + + +@overload +def trace_sunbeam_charm( + *, + tracing_endpoint: str = "get_tracing_endpoint", + server_cert: Optional[str] = None, + service_name: Optional[str] = None, + extra_types: Sequence[type] = (), +) -> Callable[[_T], _T]: + ... # fmt: skip + + +@overload +def trace_sunbeam_charm( + charm_cls: _T, + /, +) -> _T: + ... # fmt: skip + + +def trace_sunbeam_charm(*args, **kwargs) -> Any: + """Decorator for tracing sunbeam charms. + + This decorator allows either decorating a charm class directly or + passing parameters to the decorator. + + Usage: + @trace_sunbeam_charm + class MyCharm(...): + ... + + or + + @trace_sunbeam_charm( + tracing_endpoint="get_tracing_endpoint", + server_cert="path/to/server.crt", + service_name="my-service", + extra_types=(MyType,), + ) + class MyCharm(...): + ... + + or + + class MyCharm(...): + ... + + MyCharm = trace_sunbeam_charm(MyCharm) + + or + + MyCharm = trace_sunbeam_charm( + tracing_endpoint="get_tracing_endpoint", + server_cert="path/to/server.crt", + service_name="my-service", + extra_types=(MyType,), + )(MyCharm) + """ + if len(args) == 1 and not kwargs: + charm_cls = args[0] + return trace_charm( + tracing_endpoint="get_tracing_endpoint", + )(charm_cls) + + return trace_charm( + tracing_endpoint=kwargs.get( + "tracing_endpoint", "get_tracing_endpoint" + ), + server_cert=kwargs.get("server_cert"), + service_name=kwargs.get("service_name"), + extra_types=kwargs.get("extra_types", ()), + )