Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions interfaces/otlp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,23 @@ from charmlibs.interfaces.otlp import OtlpProvider
class MyOtlpServer(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.otlp_provider = OtlpProvider(self)
self.framework.observe(self.on.ingress_ready, self._on_ingress_ready)

def _on_ingress_ready(self, event):
self.otlp_provider.add_endpoint(
protocol="grpc",
endpoint="https://my-app.ingress:4317",
telemetries=["logs", "metrics"],
)
self.otlp_provider.add_endpoint(
protocol="http",
endpoint="https://my-app.ingress:4318",
telemetries=["traces"],
)
# publish the registered endpoints to the relation databag
self.otlp_provider.publish()
# optionally, get the alerting and recording rules
promql_rules = self.otlp_provider.rules("promql")
logql_rules = self.otlp_provider.rules("logql")
self.framework.observe(self.on.ingress_ready, self._publish_endpoints)
self.framework.observe(self.on.update_status, self._access_rules)

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

def _access_rules(self, event):
OtlpProvider(self).rules("promql")
OtlpProvider(self).rules("logql")
```

### Requirer Side
Expand All @@ -53,22 +51,24 @@ class MyOtlpServer(CharmBase):
from charmlibs.interfaces.otlp import OtlpRequirer

class MyOtlpSender(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.otlp_requirer = OtlpRequirer(
def __init__(self, framework: ops.Framework):
super().__init__(framework)
self.framework.observe(self.on.update_status, self._access_endpoints)
self.framework.observe(self.on.update_status, self._publish_rules)

def _publish_rules(self, _: ops.EventBase):
OtlpRequirer(
self,
protocols=["grpc", "http"],
telemetries=["logs", "metrics", "traces"],
loki_rules_path="./src/loki_alert_rules",
prometheus_rules_path="./src/prometheus_alert_rules",
)
self.framework.observe(self.on.update_status, self._reconcile)

def _reconcile(self, event):
# publish the rules to the relation databag
self.otlp_requirer.publish()
# get the endpoints from the provider
supported_endpoints = self.otlp_requirer.endpoints
).publish()

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

## Documentation
Expand Down
60 changes: 36 additions & 24 deletions interfaces/otlp/src/charmlibs/interfaces/otlp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,18 @@
class MyOtlpServer(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.otlp_provider = OtlpProvider(self)
self.framework.observe(self.on.ingress_ready, self._on_ingress_ready)
self.framework.observe(self.on.ingress_ready, self._publish_endpoints)

def _on_ingress_ready(self, event):
self.otlp_provider.add_endpoint(
def _publish_endpoints(self, event):
OtlpProvider(self).add_endpoint(
protocol="grpc",
endpoint="https://my-app.ingress:4317",
telemetries=["logs", "metrics"],
)
self.otlp_provider.add_endpoint(
).add_endpoint(
protocol="http",
endpoint="https://my-app.ingress:4318",
telemetries=["traces"],
)
self.otlp_provider.publish()
).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
Expand All @@ -61,9 +58,16 @@ def _on_ingress_ready(self, event):
The OtlpProvider also consumes rules from related OtlpRequirer charms, which can be retrieved with
the ``rules()`` method::

# snip ...
promql_rules = self.otlp_provider.rules("promql")
logql_rules = self.otlp_provider.rules("logql")
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):
OtlpProvider(self).rules("promql")
OtlpProvider(self).rules("logql")

Requirer Side (Charms requiring OTLP endpoints)
-----------------------------------------------
Expand All @@ -74,19 +78,16 @@ def _on_ingress_ready(self, event):
from charmlibs.interfaces.otlp import OtlpRequirer

class MyOtlpSender(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.otlp_requirer = OtlpRequirer(
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"],
loki_rules_path="./src/loki_alert_rules",
prometheus_rules_path="./src/prometheus_alert_rules",
)
self.framework.observe(self.on.update_status, self._reconcile)

def _reconcile(self, event):
supported_endpoints = self.otlp_requirer.endpoints
).endpoints

Given the defined, supported protocols and telemetries, the OtlpRequirer will filter out
unsupported endpoints and prune unsupported telemetries. After filtering, requirer selection
Expand All @@ -98,8 +99,19 @@ def _reconcile(self, event):
The OtlpRequirer also publishes rules to related OtlpProvider charms with the ``publish()``
method::

# snip ...
self.otlp_requirer.publish()
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._publish_rules)

def _publish_rules(self, _: ops.EventBase):
OtlpRequirer(
self,
loki_rules_path="./src/loki_alert_rules",
prometheus_rules_path="./src/prometheus_alert_rules",
).publish()

It is the charm's responsibility to manage the rules in the ``loki_rules_path`` and
``prometheus_rules_path`` directories, which will be forwarded to the related OtlpProvider charms.
Expand Down Expand Up @@ -135,7 +147,7 @@ def _reconcile(self, event):
"model": "my-model",
"model_uuid": "f4d59020-c8e7-4053-8044-a2c1e5591c7f",
"application": "my-app",
"charm": "my-charm",
"charm_name": "my-charm",
"unit": "my-charm/0",
}
"""
Expand Down
18 changes: 9 additions & 9 deletions interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ class _OtlpEndpoint(OtlpEndpoint):
"""A pydantic model for a single OTLP endpoint."""


class OtlpProviderAppData(BaseModel):
class _OtlpProviderAppData(BaseModel):
"""A pydantic model for the OTLP provider's app databag."""

endpoints: list[_OtlpEndpoint] = Field(
description='List of OTLP endpoints exposed by the provider.'
)


class OtlpRequirerAppData(BaseModel):
class _OtlpRequirerAppData(BaseModel):
"""A pydantic model for the OTLP requirer's app databag.

The rules are compressed when saved to databag to avoid hitting databag
Expand Down Expand Up @@ -234,7 +234,7 @@ def publish(self):
prom_rules.add_path(self._prom_rules_path, recursive=True)

# Publish to databag
databag = OtlpRequirerAppData.model_validate({
databag = _OtlpRequirerAppData.model_validate({
'rules': {'logql': loki_rules.as_dict(), 'promql': prom_rules.as_dict()},
'metadata': self._topology.as_dict(),
})
Expand All @@ -259,11 +259,10 @@ def endpoints(self) -> dict[int, OtlpEndpoint]:
continue

try:
provider = relation.load(OtlpProviderAppData, relation.app)
provider = relation.load(_OtlpProviderAppData, relation.app)
except ValidationError as e:
logger.error('OTLP databag failed validation: %s', e)
continue

if endpoints := self._filter_endpoints(provider.endpoints):
endpoint_map[relation.id] = self._favor_modern_endpoints(endpoints)

Expand Down Expand Up @@ -293,26 +292,27 @@ def add_endpoint(
protocol: Literal['http', 'grpc'],
endpoint: str,
telemetries: Sequence[Literal['logs', 'metrics', 'traces']],
):
) -> 'OtlpProvider':
"""Add an OtlpEndpoint to the list of endpoints to publish.

Call this method after endpoint-changing events e.g. TLS and ingress.
"""
self._endpoints.append(
_OtlpEndpoint(protocol=protocol, endpoint=endpoint, telemetries=telemetries)
)
return self

def publish(self) -> None:
"""Triggers programmatically the update of the relation data."""
if not self._charm.unit.is_leader():
# Only the leader unit can write to app data.
return

databag = OtlpProviderAppData.model_validate({'endpoints': self._endpoints})
databag = _OtlpProviderAppData.model_validate({'endpoints': self._endpoints})
for relation in self._charm.model.relations[self._relation_name]:
relation.save(databag, self._charm.app)

def rules(self, query_type: Literal['logql', 'promql']):
def rules(self, query_type: Literal['logql', 'promql']) -> dict[str, dict[str, Any]]:
"""Fetch rules for all relations of the desired query and rule types.

This method returns all rules of the desired query and rule types
Expand All @@ -336,7 +336,7 @@ def rules(self, query_type: Literal['logql', 'promql']):
continue

try:
requirer = relation.load(OtlpRequirerAppData, relation.app)
requirer = relation.load(_OtlpRequirerAppData, relation.app)
except ValidationError as e:
logger.error('OTLP databag failed validation: %s', e)
continue
Expand Down
Loading
Loading