tracing

Provide and consume tracing data using the tracing interface.

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 charmlibs.interfaces.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 requirer 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.

We recommend that you scale up your tracing provider and relate it to an ingress so that your tracing requests go through the ingress and get load balanced across all units. Otherwise, if the provider’s leader goes down, your tracing goes down.

Provider 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 charmlibs.interfaces.tracing import TracingEndpointProvider

def __init__(self, *args):
    super().__init__(*args)
    # ...
    self.tracing = TracingEndpointProvider(self)
    # ...
exception AmbiguousRelationUsageError

Bases: TracingError

Raised when one wrongly assumes that there can only be one relation on an endpoint.

class BrokenEvent(
handle: Handle,
relation: Relation,
app: Application | None = None,
unit: Unit | None = None,
)

Bases: RelationBrokenEvent

Event emitted when a relation on tracing is broken.

exception DataAccessPermissionError

Bases: TracingError

Raised when follower units attempt leader-only operations.

exception DataValidationError

Bases: TracingError

Raised when data validation fails on IPU relation data.

class DatabagModel

Bases: BaseModel

Base databag model.

classmethod load(databag: MutableMapping)

Load this model from a Juju databag.

dump(
databag: MutableMapping | None = None,
clear: bool = True,
)

Write the contents of this model to Juju databag.

Parameters:
  • databag – the databag to write the data to.

  • clear – ensure the databag is cleared before writing it.

class EndpointChangedEvent(handle, relation, *args, **kwargs)

Bases: _AutoSnapshotEvent

Event representing a change in one of the receiver endpoints.

__args__: tuple[str, ...] = ('_receivers',)
property receivers: list[Receiver]

Cast receivers back from dict.

class EndpointRemovedEvent(
handle: Handle,
relation: Relation,
app: Application | None = None,
unit: Unit | None = None,
)

Bases: RelationBrokenEvent

Event representing a change in one of the receiver endpoints.

exception NotReadyError

Bases: TracingError

Raised by the provider wrapper if a requirer hasn’t published the required data (yet).

exception ProtocolNotRequestedError

Bases: TracingError

Raised if the user attempts to obtain an endpoint for a protocol it did not request.

class ProtocolType(*, name: str, type: TransportProtocolType)

Bases: BaseModel

Protocol Type.

name: str
type: TransportProtocolType
class Receiver(*, protocol: ProtocolType, url: str)

Bases: BaseModel

Specification of an active receiver.

protocol: ProtocolType
url: str
exception RelationInterfaceMismatchError(
relation_name: str,
expected_relation_interface: str,
actual_relation_interface: str,
)

Bases: Exception

Raised if the relation with the given name has an unexpected interface.

exception RelationNotFoundError(relation_name: str)

Bases: Exception

Raised if no relation with the given name is found.

exception RelationRoleMismatchError(
relation_name: str,
expected_relation_role: RelationRole,
actual_relation_role: RelationRole,
)

Bases: Exception

Raised if the relation with the given name has a different role than expected.

class RequestEvent(
handle: Handle,
relation: Relation,
app: Application | None = None,
unit: Unit | None = None,
)

Bases: RelationEvent

Event emitted when a remote requests a tracing endpoint.

property requested_receivers: list[Literal['zipkin', 'otlp_grpc', 'otlp_http', 'jaeger_grpc', 'jaeger_thrift_http']]

List of receiver protocols that have been requested.

class TracingEndpointProvider(
charm: CharmBase,
external_url: str | None = None,
relation_name: str = 'tracing',
)

Bases: Object

Class representing a trace receiver service.

on

TracingEndpointProvider events.

is_requirer_ready(relation: Relation)

Attempt to determine if requirer has already populated app data.

requested_protocols()

All receiver protocols that have been requested by our related apps.

property relations: list[Relation]

All relations active on this endpoint.

publish_receivers(
receivers: Sequence[tuple[Literal['zipkin', 'otlp_grpc', 'otlp_http', 'jaeger_grpc', 'jaeger_thrift_http'], str]],
)

Let all requirers know that these receivers are active and listening.

class TracingEndpointProviderEvents(
parent: Object | None = None,
key: str | None = None,
)

Bases: CharmEvents

TracingEndpointProvider events.

request

EventSource wraps an event type with a descriptor to facilitate observing and emitting.

It is generally used as:

class SomethingHappened(ops.EventBase):
    pass

class SomeObject(Object):
    something_happened = ops.EventSource(SomethingHappened)

With that, instances of that type will offer the someobj.something_happened attribute which is a BoundEvent, and may be used to emit and observe the event.

broken

EventSource wraps an event type with a descriptor to facilitate observing and emitting.

It is generally used as:

class SomethingHappened(ops.EventBase):
    pass

class SomeObject(Object):
    something_happened = ops.EventSource(SomethingHappened)

With that, instances of that type will offer the someobj.something_happened attribute which is a BoundEvent, and may be used to emit and observe the event.

class TracingEndpointRequirer(
charm: CharmBase,
relation_name: str = 'tracing',
protocols: list[Literal['zipkin', 'otlp_grpc', 'otlp_http', 'jaeger_grpc', 'jaeger_thrift_http']] | None = None,
)

Bases: Object

A tracing endpoint for Tempo.

on

TracingEndpointRequirer events.

request_protocols(
protocols: Sequence[Literal['zipkin', 'otlp_grpc', 'otlp_http', 'jaeger_grpc', 'jaeger_thrift_http']],
relation: Relation | None = None,
)

Publish the list of protocols which the provider should activate.

property relations: list[Relation]

The tracing relations associated with this endpoint.

is_ready(relation: Relation | None = None)

Is this endpoint ready?

get_all_endpoints(
relation: Relation | None = None,
) TracingProviderAppData | None

Unmarshalled relation data.

get_endpoint(
protocol: Literal['zipkin', 'otlp_grpc', 'otlp_http', 'jaeger_grpc', 'jaeger_thrift_http'],
relation: Relation | None = None,
) str | None

Receiver endpoint for the given protocol.

It could happen that this function gets called before the provider publishes the endpoints. In such a scenario, if a non-leader unit calls this function, a permission denied exception will be raised due to restricted access. To prevent this, this function needs to be guarded by the is_ready check.

Raises:

ProtocolNotRequestedError – If the charm unit is the leader unit and attempts to obtain an endpoint for a protocol it did not request.

class TracingEndpointRequirerEvents(
parent: Object | None = None,
key: str | None = None,
)

Bases: CharmEvents

TracingEndpointRequirer events.

endpoint_changed

EventSource wraps an event type with a descriptor to facilitate observing and emitting.

It is generally used as:

class SomethingHappened(ops.EventBase):
    pass

class SomeObject(Object):
    something_happened = ops.EventSource(SomethingHappened)

With that, instances of that type will offer the someobj.something_happened attribute which is a BoundEvent, and may be used to emit and observe the event.

endpoint_removed

EventSource wraps an event type with a descriptor to facilitate observing and emitting.

It is generally used as:

class SomethingHappened(ops.EventBase):
    pass

class SomeObject(Object):
    something_happened = ops.EventSource(SomethingHappened)

With that, instances of that type will offer the someobj.something_happened attribute which is a BoundEvent, and may be used to emit and observe the event.

exception TracingError

Bases: Exception

Base class for custom errors raised by this library.

class TracingProviderAppData(*, receivers: list[Receiver])

Bases: DatabagModel

Application databag model for the tracing provider.

receivers: list[Receiver]
class TracingRequirerAppData(
*,
receivers: list[Literal['zipkin', 'otlp_grpc', 'otlp_http', 'jaeger_grpc', 'jaeger_thrift_http']],
)

Bases: DatabagModel

Application databag model for the tracing requirer.

receivers: list[Literal['zipkin', 'otlp_grpc', 'otlp_http', 'jaeger_grpc', 'jaeger_thrift_http']]

Requested receivers.

class TransportProtocolType(
value,
names=<not given>,
*values,
module=None,
qualname=None,
type=None,
start=1,
boundary=None,
)

Bases: str, Enum

Receiver Type.

http = 'http'
grpc = 'grpc'
charm_tracing_config(
endpoint_requirer: TracingEndpointRequirer,
cert_path: Path | str | None,
) tuple[str | None, str | None]

Return the charm_tracing config you 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:

>>> from lib.charms.tempo_coordinator_k8s.v0.charm_tracing import trace_charm
>>> from charmlibs.interfaces.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)