Implement tracing

Implement tracing relation for all the charms.
Instrument most of ops.Object objects, including relation handlers,
pebble handlers, and relation objects.

Change-Id: I967ff858a63aa7d30094cf5a46491fce11195060
Signed-off-by: Guillaume Boutry <guillaume.boutry@canonical.com>
This commit is contained in:
Guillaume Boutry 2024-07-31 11:11:29 +02:00
parent bd0990f2ad
commit e5fb16d6b9
No known key found for this signature in database
GPG Key ID: E95E3326872E55DE
92 changed files with 2303 additions and 39 deletions

View File

@ -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:

View File

@ -78,6 +78,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
aodh:

View File

@ -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."""

View File

@ -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

View File

@ -48,6 +48,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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

View File

@ -55,6 +55,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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

View File

@ -45,6 +45,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
ceph-access:

View File

@ -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."""

View File

@ -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:

View File

@ -63,6 +63,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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

View File

@ -35,6 +35,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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

View File

@ -55,6 +55,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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:

View File

@ -74,6 +74,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
image-service:

View File

@ -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."""

View File

@ -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:

View File

@ -57,6 +57,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
gnocchi-service:

View File

@ -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."""

View File

@ -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

View File

@ -60,6 +60,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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:

View File

@ -49,6 +49,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
horizon:

View File

@ -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."""

View File

@ -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

View File

@ -51,6 +51,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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

View File

@ -16,3 +16,7 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1

View File

@ -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."""

View File

@ -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

View File

@ -57,6 +57,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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

View File

@ -66,6 +66,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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:

View File

@ -99,6 +99,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
nova-service:

View File

@ -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."""

View File

@ -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

View File

@ -82,6 +82,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
peers:
peers:

View File

@ -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."""

View File

@ -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:

View File

@ -34,6 +34,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
metrics-endpoint:

View File

@ -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."""

View File

@ -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

View File

@ -28,6 +28,10 @@ requires:
optional: true
nova-service:
interface: nova
tracing:
interface: tracing
optional: true
limit: 1
provides:
cos-agent:

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -72,6 +72,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
ovsdb:

View File

@ -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."""

View File

@ -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

View File

@ -37,6 +37,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
ovsdb-cms-relay:

View File

@ -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."""

View File

@ -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:

View File

@ -46,6 +46,10 @@ requires:
logging:
interface: loki_push_api
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
placement:

View File

@ -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."""

View File

@ -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

View File

@ -57,3 +57,7 @@ requires:
certificates:
interface: tls-certificates
optional: True
tracing:
interface: tracing
optional: true
limit: 1

View File

@ -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."""

View File

@ -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)

View File

@ -1,2 +1,4 @@
external-libraries:
- charms.operator_libs_linux.v0.sysctl
- charms.tempo_k8s.v2.tracing
- charms.tempo_k8s.v1.charm_tracing

View File

@ -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

View File

@ -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."""

View File

@ -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:

View File

@ -65,6 +65,10 @@ requires:
receive-ca-cert:
interface: certificate_transfer
optional: true
tracing:
interface: tracing
optional: true
limit: 1
provides:
grafana-dashboard:

View File

@ -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."""

View File

@ -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,

View File

@ -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."
)

View File

@ -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

View File

@ -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()]

View File

@ -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.

View File

@ -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."""

View File

@ -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."""

View File

@ -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."""

View File

@ -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."""

View File

@ -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."""

View File

@ -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,
)

View File

@ -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()

View File

@ -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", ()),
)