Skip to content
Open
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
5 changes: 4 additions & 1 deletion interfaces/otlp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ classifiers = [
dynamic = ["version"]
dependencies = [
# "ops",
"cosl>=1.6.1",
"cosl @ git+https://github.com/swetha1654/cos-lib@feat/rewrite-rules",
"requests",
]

Expand All @@ -42,6 +42,9 @@ integration = [ # installed for `just integration otlp`
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.build.targets.wheel]
packages = ["src/charmlibs"]

Expand Down
37 changes: 23 additions & 14 deletions interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,21 @@
import logging
import re
from collections import OrderedDict
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Final, Literal
from typing import Any, Final, Literal, cast

from cosl.backends.loki import LokiRuleBackend
from cosl.backends.prometheus import PrometheusRuleBackend
from cosl.cos_tool import CosTool
from cosl.juju_topology import JujuTopology
from cosl.rules import HOST_METRICS_MISSING_RULE_NAME, Rules, generic_alert_groups
from cosl.types import OfficialRuleFileFormat, SingleRuleFormat
from cosl.rules import (
HOST_METRICS_MISSING_RULE_NAME,
GenericRules,
generic_alert_groups,
)
from cosl.types import OfficialRuleFileFormat, OfficialRuleFileItem, SingleRuleFormat
from cosl.utils import LZMABase64
from ops import CharmBase
from pydantic import (
Expand All @@ -56,12 +63,12 @@ class RuleStore:
"""An API for users to provide rules of different types to the OtlpRequirer."""

topology: JujuTopology
logql: Rules = field(init=False)
promql: Rules = field(init=False)
logql: GenericRules[OfficialRuleFileItem] = field(init=False)
promql: GenericRules[OfficialRuleFileItem] = field(init=False)

def __post_init__(self):
self.logql = Rules(query_type='logql', topology=self.topology)
self.promql = Rules(query_type='promql', topology=self.topology)
self.logql = GenericRules(backend=LokiRuleBackend(topology=self.topology))
self.promql = GenericRules(backend=PrometheusRuleBackend(topology=self.topology))

def add_logql(
self,
Expand Down Expand Up @@ -319,7 +326,9 @@ def _duplicate_rules_per_unit(
for juju_unit in sorted(peer_unit_names):
rule_copy = copy.deepcopy(rule)
rule_copy.get('labels', {})['juju_unit'] = juju_unit
rule_copy['expr'] = self._rules.promql.tool.inject_label_matchers(
rule_copy['expr'] = CosTool(
default_query_type='promql'
).inject_label_matchers(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self._rules.promql is of type GenericRules now and doesn't expose a tool attribute anymore so we can either use self._rules.promql.backend.tool.inject_label_matchers or we can use CosTool directly. I decided to use CosTool to keep it simple.

expression=re.sub(r'%%juju_unit%%,?', '', rule_copy['expr']),
topology={'juju_unit': juju_unit},
)
Expand Down Expand Up @@ -488,13 +497,13 @@ def rules(self) -> dict[int, RuleStore]:
logger.error('OTLP databag failed validation: %s', e)
continue

# Create a RuleStore for this relation's rules, and inject topology labels
# Validate and build RuleStore from requirer rules
rules = RuleStore(self._topology)
logql_result = rules.logql.inject_and_validate_rules(
requirer.rules.logql, requirer.metadata
logql_result = rules.logql.validate(
cast('Mapping[str, list[OfficialRuleFileItem]]', requirer.rules.logql)
)
promql_result = rules.promql.inject_and_validate_rules(
requirer.rules.promql, requirer.metadata
promql_result = rules.promql.validate(
cast('Mapping[str, list[OfficialRuleFileItem]]', requirer.rules.promql)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new GenericRules class does not expose inject_and_validate but only validate. We can be sure that the rules obtained from OtlpRequirer databag will always have juju topology injected. This is because the OtlpRequirer class uses the add API from the upstream cosl before it dumps into the databag and this method takes care of adding the juju topology.

)
if logql_result.rules and not logql_result.errmsg:
rules.logql.add(logql_result.rules)
Expand Down
40 changes: 5 additions & 35 deletions interfaces/otlp/tests/unit/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,23 +204,8 @@ def test_metadata(otlp_requirer_ctx: testing.Context[ops.CharmBase]):
}


@pytest.mark.parametrize(
'metadata',
[
{},
{
'model': MODEL_NAME,
'model_uuid': MODEL_UUID,
'application': 'otlp-requirer',
'charm_name': 'otlp-requirer',
'unit': 'otlp-requirer/0',
},
],
)
def test_provider_rules(
otlp_provider_ctx: testing.Context[ops.CharmBase], metadata: dict[str, Any]
):
# GIVEN a requirer offers unlabeled rules (of various types) in the databag
def test_provider_rules(otlp_provider_ctx: testing.Context[ops.CharmBase]):
# GIVEN a requirer offers rules in the databag
rules = {
'logql': {
'groups': [
Expand All @@ -236,7 +221,7 @@ def test_provider_rules(
},
}
receiver = Relation(
RECEIVE, remote_app_data={'rules': json.dumps(rules), 'metadata': json.dumps(metadata)}
RECEIVE, remote_app_data={'rules': json.dumps(rules), 'metadata': json.dumps({})}
)
state = State(leader=True, relations=[receiver], model=MODEL)
with otlp_provider_ctx(otlp_provider_ctx.on.update_status(), state=state) as mgr:
Expand All @@ -247,23 +232,8 @@ def test_provider_rules(
# THEN LogQL and PromQL rules exist in the RuleStore
assert logql
assert promql
for result in [logql, promql]:
app = metadata['application'] if metadata else 'otlp-provider'
charm = metadata['charm_name'] if metadata else 'otlp-provider'
groups = result.get('groups', [])
assert groups
for group in groups:
for rule in group.get('rules', []):
# AND the rules are labeled with the provider's topology
assert rule.get('labels', {}).get('juju_model') == MODEL_NAME
assert rule.get('labels', {}).get('juju_model_uuid') == MODEL_UUID
assert rule.get('labels', {}).get('juju_application') == app
assert rule.get('labels', {}).get('juju_charm') == charm

# AND the expressions are labeled
assert f'juju_model="{MODEL_NAME}"' in rule['expr']
assert f'juju_model_uuid="{MODEL_UUID}"' in rule['expr']
assert f'juju_application="{app}"' in rule['expr']

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the provier does not do topology injection anymore so we can remove this part of the test

assert logql.get('groups')
assert promql.get('groups')


def _make_store() -> RuleStore:
Expand Down
10 changes: 3 additions & 7 deletions interfaces/otlp/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading