Skip to content

Commit ec17c85

Browse files
committed
address review comments
1 parent 470b390 commit ec17c85

File tree

6 files changed

+119
-57
lines changed

6 files changed

+119
-57
lines changed

ops/_tracing/__init__.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from __future__ import annotations
2020

21+
import warnings
2122
from contextlib import contextmanager
2223
from dataclasses import dataclass
2324
from typing import Generator
@@ -29,12 +30,14 @@
2930

3031
tracer = opentelemetry.trace.get_tracer('ops', ops.version.version)
3132

33+
3234
@dataclass
3335
class _Config:
3436
"""Tracing destination configuration.
3537
3638
NOTE: that empty string values may be coerced to None.
3739
"""
40+
3841
url: str | None
3942
"""The URL to send tracing data to."""
4043
ca: str | None
@@ -50,9 +53,19 @@ class _Config:
5053
except ImportError:
5154

5255
def mark_observed() -> None: ...
53-
def set_tracing_destination(config: _Config) -> None: ...
56+
57+
def set_tracing_destination(config: _Config) -> None:
58+
warnings.warn(
59+
'Tracing is not enabled, but set_tracing_destination() was called. '
60+
"Ensure 'ops[tracing]' is installed.",
61+
UserWarning,
62+
stacklevel=3,
63+
)
64+
5465
@contextmanager
55-
def setup_tracing(juju_context: _JujuContext, charm_class_name: str) -> Generator[None, None, None]:
66+
def setup_tracing(
67+
juju_context: _JujuContext, charm_class_name: str
68+
) -> Generator[None, None, None]:
5669
yield
5770

5871

ops/_tracing/api.py

+76-47
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import ops
2121
from ops._tracing import _Config
22+
from ops.charm import RelationRole
2223

2324
from .vendor.charms.certificate_transfer_interface.v1.certificate_transfer import (
2425
CertificateTransferRequires,
@@ -44,7 +45,7 @@
4445
# ```
4546
# {"receivers": ["otlp_http"]}
4647
# ```
47-
#
48+
#
4849
# - client reads this from remote_app_data:
4950
# ```
5051
# {"receivers": [
@@ -58,7 +59,7 @@
5859

5960

6061
class Tracing(ops.Object):
61-
"""FIXME docstring."""
62+
"""Tracing service."""
6263

6364
_certificate_transfer: CertificateTransferRequires | None
6465

@@ -69,46 +70,70 @@ def __init__(
6970
ca_relation_name: str | None = None,
7071
ca_data: str | None = None,
7172
):
72-
"""FIXME docstring.
73-
74-
Include `ops[tracing]` in your dependencies.
75-
76-
Declare the relations that the charm supports:
77-
78-
```yaml
79-
requires:
80-
charm-tracing:
81-
interface: tracing
82-
limit: 1
83-
optional: true
84-
send-ca-cert:
85-
interface: certificate_transfer
86-
limit: 1
87-
optional: true
88-
```
89-
90-
Initialise the tracing thing (FIXME) with the names of these relations.
91-
92-
```py
93-
import ops.tracing
94-
95-
class SomeCharm(ops.CharmBase):
96-
def __init__(self, framework: ops.Framework):
97-
...
98-
self._tracing = ops.tracing.Tracing(
99-
tracing_relation_name="charm-tracing",
100-
ca_relation_name="send-ca-cart",
101-
)
102-
```
73+
"""Initialise the tracing service.
74+
75+
Args:
76+
charm: your charm instange
77+
tracing_relation_name: the name of the relation that provides the
78+
destination to send tracing data to.
79+
ca_relation_name: the name of the relation that provides the CA
80+
list to validate the tracing destination against.
81+
ca_data: a fixed CA list (PEM bundle, a multi-line string).
82+
83+
If the destination is resolved to an HTTPS URL, a CA list is required
84+
to establish a secure connection.
85+
86+
The CA list can be provided over a relation via ``ca_relation_name=``
87+
argument, as a fixed string via ``ca_data`` argument, or the system CA
88+
list will be used if the earlier two are both ``None``.
89+
90+
Usage:
91+
- Include ``ops[tracing]`` in your dependencies.
92+
- Declare the relations that the charm supports.
93+
- Initialise ``Tracing`` with the names of these relations.
94+
95+
Example::
96+
# charmcraft.yaml
97+
requires:
98+
charm-tracing:
99+
interface: tracing
100+
limit: 1
101+
optional: true
102+
send-ca-cert:
103+
interface: certificate_transfer
104+
limit: 1
105+
optional: true
106+
107+
# src/charm.py
108+
import ops.tracing
109+
110+
class SomeCharm(ops.CharmBase):
111+
def __init__(self, framework: ops.Framework):
112+
...
113+
self.tracing = ops.tracing.Tracing(
114+
tracing_relation_name="charm-tracing",
115+
ca_relation_name="send-ca-cart",
116+
)
103117
"""
104118
super().__init__(charm, f'{tracing_relation_name}+{ca_relation_name}')
105119
self.charm = charm
106120
self.tracing_relation_name = tracing_relation_name
107121
self.ca_relation_name = ca_relation_name
108122
self.ca_data = ca_data
109123

110-
# FIXME: Pietro recommends inspecting charm meta to validate the interface name
111-
assert self.charm.meta.relations[tracing_relation_name].interface_name == 'tracing'
124+
# NOTE: Pietro recommends inspecting charm meta to validate the relation
125+
# that way a badly written charm crashes in early testing.
126+
if not (relation := self.charm.meta.relations.get(tracing_relation_name)):
127+
raise ValueError(f'{tracing_relation_name} is not declared in charm metadata')
128+
if (relation_role := relation.role) is not RelationRole.requires:
129+
raise ValueError(
130+
f"{tracing_relation_name} {relation_role=} when 'requires' is expected"
131+
)
132+
if (interface_name := relation.interface_name) != 'tracing':
133+
raise ValueError(
134+
f"{tracing_relation_name=} {interface_name=} when 'tracing' is expected"
135+
)
136+
112137
self._tracing = TracingEndpointRequirer(
113138
self.charm,
114139
tracing_relation_name,
@@ -121,12 +146,7 @@ def __init__(self, framework: ops.Framework):
121146
# RelationRoleMismatchError,
122147

123148
for event in (
124-
# FIXME: validate this
125-
# - the start event may happen after relation-joined?
126-
# - the k8s container died and got restarted
127-
# - the vm [smth] crashed and got restarted
128149
self.charm.on.start,
129-
# In case the previous charm version has a relation but didn't save the URL
130150
self.charm.on.upgrade_charm,
131151
self._tracing.on.endpoint_changed,
132152
self._tracing.on.endpoint_removed,
@@ -135,11 +155,20 @@ def __init__(self, framework: ops.Framework):
135155

136156
self._certificate_transfer = None
137157
if ca_relation_name:
158+
if not (relation := self.charm.meta.relations.get(ca_relation_name)):
159+
raise ValueError(f'{ca_relation_name} is not declared in charm metadata')
160+
if (relation_role := relation.role) is not RelationRole.requires:
161+
raise ValueError(
162+
f"{ca_relation_name} {relation_role=} when 'requires' is expected"
163+
)
164+
if (interface_name := relation.interface_name) != 'certificate_transfer':
165+
raise ValueError(
166+
f"{ca_relation_name=} {interface_name=} when 'certificate_transfer' "
167+
'is expected'
168+
)
169+
138170
self._certificate_transfer = CertificateTransferRequires(charm, ca_relation_name)
139-
assert (
140-
self.charm.meta.relations[ca_relation_name].interface_name
141-
== 'certificate_transfer'
142-
)
171+
143172
for event in (
144173
self._certificate_transfer.on.certificate_set_updated,
145174
self._certificate_transfer.on.certificates_removed,
@@ -168,8 +197,8 @@ def _get_config(self) -> _Config:
168197
ca_rel_id = ca_rel.id if ca_rel else None
169198

170199
if ca_rel and self._certificate_transfer.is_ready(ca_rel):
171-
return _Config(url, '\n'.join(
172-
sorted(self._certificate_transfer.get_all_certificates(ca_rel_id))
173-
))
200+
return _Config(
201+
url, '\n'.join(sorted(self._certificate_transfer.get_all_certificates(ca_rel_id)))
202+
)
174203
else:
175204
return _Config(None, None)

ops/_tracing/buffer.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,17 @@ def tx(self, *, timeout: float = DB_TIMEOUT, readonly: bool = False):
149149
def get_destination(self) -> _Config:
150150
with self.tx(readonly=True) as conn:
151151
settings = {k: v for k, v in conn.execute("""SELECT key, value FROM settings""")}
152-
return _Config(settings.get("url") or None, settings.get("ca") or None)
152+
return _Config(settings.get('url') or None, settings.get('ca') or None)
153153

154154
@retry
155155
def set_destination(self, config: _Config) -> None:
156156
with self.tx() as conn:
157-
conn.execute("""REPLACE INTO settings(key, value) VALUES('url', ?)""", (config.url or "",))
158-
conn.execute("""REPLACE INTO settings(key, value) VALUES('ca', ?)""", (config.ca or "",))
157+
conn.execute(
158+
"""REPLACE INTO settings(key, value) VALUES('url', ?)""", (config.url or '',)
159+
)
160+
conn.execute(
161+
"""REPLACE INTO settings(key, value) VALUES('ca', ?)""", (config.ca or '',)
162+
)
159163

160164
# FIXME: currently unused
161165
# If most charms observe the CollectStatusEvent, then an event is observed on every dispatch.

ops/_tracing/export.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ def suppress_juju_log_handler():
196196

197197

198198
@contextlib.contextmanager
199-
def setup_tracing(juju_context: _JujuContext, charm_class_name: str) -> Generator[None, None, None]:
199+
def setup_tracing(
200+
juju_context: _JujuContext, charm_class_name: str
201+
) -> Generator[None, None, None]:
200202
global _exporter
201203
# FIXME is it ever possible for unit_name to be unset (empty)?
202204
app_name, unit_number = juju_context.unit_name.split('/', 1)
@@ -234,9 +236,9 @@ def set_tracing_destination(config: _Config) -> None:
234236
"""Configure the destination service for tracing data.
235237
236238
Args:
237-
url: The URL of the telemetry service to send tracing data to.
238-
ca: The PEM formatted CA list.
239-
Only in use if the URL is an HTTPS URL.
239+
config: destination configuration with two fields: ``.url``, the URL of the
240+
telemetry service to send tracing data to and ``.ca``, the CA list (PEM
241+
bundle, a multi-line string) if the URL is an HTTPS URL.
240242
"""
241243
if not _exporter:
242244
return

test/fixme_charm/test/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
sys.path.insert(0, '')
2828

2929

30-
import ops.testing
3130
import ops._tracing
31+
import ops.testing
3232
from ops.jujucontext import _JujuContext
3333

3434

tracing/ops_tracing/__init__.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2025 Canonical Ltd.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""The tracing facility of the Operator Framework."""

0 commit comments

Comments
 (0)