otlp

OTLP Provider and Requirer Library.

OTLP is a general-purpose telemetry data delivery protocol defined by the design goals and requirements of the project.

This library provides a way for charms to share OTLP endpoint information, and associated Loki and Prometheus rules. This library requires that the charm’s workload already supports sending/receiving OTLP data and focuses on communicating those endpoints.

Getting Started

Provider Side (Charms offering OTLP endpoints)

To provide OTLP endpoints, use the OtlpProvider class. Configure and send endpoints with the add_endpoint and publish() methods:

from charmlibs.interfaces.otlp import OtlpProvider

class MyOtlpServer(CharmBase):
    def __init__(self, *args):
        super().__init__(*args)
        self.framework.observe(self.on.ingress_ready, self._publish_endpoints)

    def _publish_endpoints(self, event):
        OtlpProvider(self).add_endpoint(
            protocol="grpc",
            endpoint="https://my-app.ingress:4317",
            telemetries=["logs", "metrics"],
        ).add_endpoint(
            protocol="http",
            endpoint="https://my-app.ingress:4318",
            telemetries=["traces"],
        ).publish()

Providers add endpoints explicitly; nothing is auto-published by default. Make sure to add endpoints and publish them after the charm’s endpoint details have been updated e.g., ingress or TLS changes.

The OtlpProvider also consumes rules from related OtlpRequirer charms, which can be retrieved with the rules property:

from charmlibs.interfaces.otlp import OtlpProvider

class MyOtlpServer(CharmBase):
    def __init__(self, *args):
        super().__init__(*args)
        self.framework.observe(self.on.update_status, self._access_rules)

    def _access_rules(self, event):
        for relation_id, rule_store in OtlpProvider(self).rules.items():
            pass  # do something with rule_store.logql and/or rule_store.promql

Requirer Side (Charms requiring OTLP endpoints)

To consume OTLP endpoints, use the OtlpRequirer class. The OTLP sender may only support a subset of protocols and telemetries, which can be configured at instantiation:

from charmlibs.interfaces.otlp import OtlpRequirer

class MyOtlpSender(CharmBase):
    def __init__(self, framework: ops.Framework):
        super().__init__(framework)
        self.framework.observe(self.on.update_status, self._access_endpoints)

    def _access_endpoints(self, _: ops.EventBase):
        OtlpRequirer(
            self,
            protocols=["grpc", "http"],
            telemetries=["logs", "metrics", "traces"],
        ).endpoints

Given the defined, supported protocols and telemetries, the OtlpRequirer will filter out unsupported endpoints and prune unsupported telemetries. After filtering, requirer selection condenses the list to a single endpoint per relation. Endpoints with modern protocols are favoured over legacy ones. That means an endpoint supporting the gRPC protocol will be selected over one supporting HTTP. Unknown protocols will receive the lowest priority.

The OtlpRequirer also publishes user-defined and generic (applied to all charms) rules to related OtlpProvider charms with the publish() method:

from charmlibs.interfaces.otlp import OtlpRequirer, RulesStore

class MyOtlpSender(CharmBase):
    def __init__(self, framework: ops.Framework):
        super().__init__(framework)
        self.framework.observe(self.on.update_status, self._publish_rules)

    def _publish_rules(self, _: ops.EventBase):
        rules = (
            RuleStore(JujuTopology.from_charm(self))
            .add_logql(SINGLE_LOGQL_ALERT, group_name='test_logql_alert')
            .add_promql(SINGLE_PROMQL_RECORD, group_name='test_promql_record')
            .add_logql(OFFICIAL_LOGQL_RULES)
        )
        OtlpRequirer(self, rules=rules).publish()

Generic rules are sourced from cosl.rules.generic_alert_groups. If the charm is an aggregator e.g., opentelemetry-collector, the type of generic rules to be injected into the charm’s RuleStore should reflect that. This is configurable by setting the aggregator_peer_relation_name with the name of the charm’s peer relation:

OtlpRequirer(..., aggregator_peer_relation_name="my-peers").publish()

Relation Data Format

The OtlpProvider offers a list of OTLP endpoints in the relation databag under the endpoints key. Each provider may offer any number of OTLP endpoints:

"endpoints": [
    {
        "protocol": "grpc",
        "endpoint": "https://my-app.ingress:4317",
        "telemetries": ["logs", "metrics"],
    },
    {
        "protocol": "http",
        "endpoint": "https://my-app.ingress:4318",
        "telemetries": ["traces"],
    },
]

The OtlpRequirer offers compressed rules in the relation databag under the rules key, which have this structure when decompressed:

"rules": {
    "promql": {...},
    "logql": {...},
}

The charm’s metadata is included under the metadata key for the provider to know the source of the rules:

"metadata": {
    "model": "my-model",
    "model_uuid": "f4d59020-c8e7-4053-8044-a2c1e5591c7f",
    "application": "my-app",
    "charm_name": "my-charm",
    "unit": "my-charm/0",
}
class OtlpEndpoint(
*,
protocol: str,
endpoint: str,
telemetries: Sequence[str],
insecure: bool = False,
)

Bases: BaseModel

A pydantic model for a single OTLP endpoint.

protocol: str
endpoint: str
telemetries: Sequence[str]
insecure: bool
class OtlpProvider(charm: CharmBase, relation_name: str = 'receive-otlp')

Bases: object

A class for publishing all supported OTLP endpoints.

Parameters:
  • charm – The charm instance.

  • relation_name – The name of the relation to use.

add_endpoint(
protocol: Literal['http', 'grpc'],
endpoint: str,
telemetries: Sequence[Literal['logs', 'metrics', 'traces']],
insecure: bool = False,
) OtlpProvider

Add an OtlpEndpoint to the list of endpoints to publish.

publish() None

Triggers programmatically the update of the relation data.

property rules: dict[int, RuleStore]

Fetch rules for all relations of the desired query and rule types.

This method returns all rules of varying query and rule types, provided by related OTLP requirer charms. This method ensures rules:

  • have labels from the charm’s Juju topology.

  • have expression labels from the charm’s Juju topology.

  • are validated using CosTool.

Returns:

a mapping of relation ID to a RuleStore object.

class OtlpRequirer(
charm: CharmBase,
relation_name: str = 'send-otlp',
protocols: Sequence[Literal['http', 'grpc']] | None = None,
telemetries: Sequence[Literal['logs', 'metrics', 'traces']] | None = None,
*,
aggregator_peer_relation_name: str | None = None,
rules: RuleStore | None = None,
)

Bases: object

A class for consuming OTLP endpoints.

Parameters:
  • charm – The charm instance.

  • relation_name – The name of the relation to use.

  • protocols – The protocols to filter for in the provider’s OTLP endpoints.

  • telemetries – The telemetries to filter for in the provider’s OTLP endpoints.

  • aggregator_peer_relation_name – Name of the peers relation of this charm. This should only be set IFF the charm is an aggregator AND it has a peer relation with this name. When provided, generic aggregator rules are used instead of application-level rules.

  • rules – Rules of different types e.g., logql or promql, that the requirer will publish for the provider.

publish()

Triggers programmatically the update of the relation data.

These rule sources are included when publishing:
  • Any rules provided at the instantiation of this class.

  • Generic (not specific to any charm) PromQL rules.

property endpoints: dict[int, OtlpEndpoint]

Return a mapping of relation ID to OTLP endpoint.

For each remote’s list of OtlpEndpoints, the requirer filters out unsupported endpoints and telemetries. If multiple compatible endpoints remain, the requirer prefers newer protocols (grpc over http). Unknown protocols are treated as the lowest priority. This allows providers to expose multiple endpoints with different protocol and telemetry combinations while the requirer selects the best match.

class RuleStore(topology: JujuTopology)

Bases: object

An API for users to provide rules of different types to the OtlpRequirer.

topology: JujuTopology
logql: Rules
promql: Rules
__post_init__()
add_logql(
rule_dict: OfficialRuleFileFormat | AlertingRuleFormat | RecordingRuleFormat,
*,
group_name: str | None = None,
group_name_prefix: str | None = None,
) RuleStore

Add rules from dict to the existing LogQL ruleset.

Parameters:
  • rule_dict – a single-rule or official-rule YAML dict

  • group_name – a custom group name, used only if the new rule is of single-rule format

  • group_name_prefix – a custom group name prefix, used only if the new rule is of single-rule format

add_logql_path(
dir_path: str | Path,
*,
recursive: bool = False,
) RuleStore

Add LogQL rules from a dir path.

All rules from files are aggregated into a data structure representing a single rule file. All group names are augmented with juju topology.

Parameters:
  • dir_path – either a rules file or a dir of rules files.

  • recursive – whether to read files recursively or not (no impact if path is a file).

add_promql(
rule_dict: OfficialRuleFileFormat | AlertingRuleFormat | RecordingRuleFormat,
*,
group_name: str | None = None,
group_name_prefix: str | None = None,
) RuleStore

Add rules from dict to the existing PromQL ruleset.

Parameters:
  • rule_dict – a single-rule or official-rule YAML dict

  • group_name – a custom group name, used only if the new rule is of single-rule format

  • group_name_prefix – a custom group name prefix, used only if the new rule is of single-rule format

add_promql_path(
dir_path: str | Path,
*,
recursive: bool = False,
) RuleStore

Add PromQL rules from a dir path.

All rules from files are aggregated into a data structure representing a single rule file. All group names are augmented with juju topology.

Parameters:
  • dir_path – either a rules file or a dir of rules files.

  • recursive – whether to read files recursively or not (no impact if path is a file).

combine(other: RuleStore) RuleStore

Combine rules from another RuleStore with this RuleStore.