Skip to content

Commit b283701

Browse files
committed
ops[tracing] with a charm lib
1 parent 33884f9 commit b283701

27 files changed

+3239
-275
lines changed

docs/howto/trace-the-charm-code.md

+2-18
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,14 @@
33

44
## Tracing from scratch
55

6-
FIXME: write this up
7-
8-
- depend on `ops[tracing]`
9-
- remove charm\_tracing charm lib, if it's installed
10-
- observe the `SetupTracingEvent`
11-
12-
```py
13-
class YourCharm(ops.CharmBase):
14-
def __init__(self, framework: ops.Framework):
15-
super().__init__(framework)
16-
self.framework.observe(self.on.setup_tracing, self._on_setup_tracing)
17-
...
18-
19-
def _on_setup_tracing(self, event: ops.SetupTracingEvent) -> None:
20-
# FIXME must get this from some relation
21-
event.set_destination(url='http://localhost:4318/v1/traces')
22-
```
6+
FIXME: copy from tracing/api.py
237

248
## Migrating from charm\_tracing
259

2610
- depend on `ops[tracing]`
2711
- remove charm\_tracing charm lib, if it's installed
2812
- remove `@trace_charm` decorator
29-
- observe the `SetupTracingEvent`
13+
- include `ops._tracing.Tracing()` in your charm's `__init__`
3014
- instrument key functions in the charm
3115

3216
NOTE: charm\_tracing auto-instruments all public function on the class. `ops[tracing]` doesn't do that.

docs/requirements.txt

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#
77
alabaster==1.0.0
88
# via sphinx
9+
annotated-types==0.7.0
10+
# via pydantic
911
anyio==4.8.0
1012
# via
1113
# starlette
@@ -101,12 +103,16 @@ opentelemetry-semantic-conventions==0.51b0
101103
# opentelemetry-sdk
102104
opentelemetry-util-http==0.51b0
103105
# via opentelemetry-instrumentation-urllib
104-
otlp-json==0.9.4
106+
otlp-json==0.9.7
105107
# via ops (pyproject.toml)
106108
packaging==24.2
107109
# via
108110
# opentelemetry-instrumentation
109111
# sphinx
112+
pydantic==2.10.6
113+
# via ops (pyproject.toml)
114+
pydantic-core==2.27.2
115+
# via pydantic
110116
pygments==2.19.1
111117
# via
112118
# furo
@@ -183,6 +189,8 @@ typing-extensions==4.12.2
183189
# via
184190
# anyio
185191
# opentelemetry-sdk
192+
# pydantic
193+
# pydantic-core
186194
uc-micro-py==1.0.3
187195
# via linkify-it-py
188196
urllib3==2.3.0

dont-merge/fake-charm.py

-96
This file was deleted.

dont-merge/metadata.yaml

-1
This file was deleted.

dont-merge/readme.md

-36
This file was deleted.

ops/__init__.py

-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@
9999
'SecretExpiredEvent',
100100
'SecretRemoveEvent',
101101
'SecretRotateEvent',
102-
'SetupTracingEvent',
103102
'StartEvent',
104103
'StopEvent',
105104
'StorageAttachedEvent',
@@ -246,7 +245,6 @@
246245
SecretExpiredEvent,
247246
SecretRemoveEvent,
248247
SecretRotateEvent,
249-
SetupTracingEvent,
250248
StartEvent,
251249
StopEvent,
252250
StorageAttachedEvent,

ops/_main.py

-1
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,6 @@ def _make_framework(self, dispatcher: _Dispatcher):
491491

492492
def _emit(self):
493493
"""Emit the event on the charm."""
494-
_charm._setup_tracing(self.charm)
495494
# TODO: Remove the collect_metrics check below as soon as the relevant
496495
# Juju changes are made. Also adjust the docstring on
497496
# EventBase.defer().

ops/_tracing/api.py

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
"""FIXME docstring."""
15+
16+
from __future__ import annotations
17+
18+
from typing import Any
19+
20+
import ops
21+
22+
from .vendor.charms.certificate_transfer_interface.v1.certificate_transfer import (
23+
CertificateTransferRequires,
24+
)
25+
from .vendor.charms.tempo_coordinator_k8s.v0.tracing import TracingEndpointRequirer
26+
27+
# NOTE: the shape of certificate_transfer is `{"certificates": list[str]}`
28+
# it is trivial to read the certificates out manually
29+
30+
31+
class Tracing(ops.Object):
32+
"""FIXME docstring."""
33+
34+
_certificate_transfer: CertificateTransferRequires | None
35+
36+
def __init__(
37+
self,
38+
charm: ops.CharmBase,
39+
tracing_relation_name: str,
40+
ca_relation_name: str | None = None,
41+
ca_data: str | None = None,
42+
):
43+
"""FIXME docstring.
44+
45+
Include `ops[tracing]` in your dependencies.
46+
47+
Declare the relations that the charm supports:
48+
49+
```yaml
50+
requires:
51+
charm-tracing:
52+
interface: tracing
53+
limit: 1
54+
send-ca-cert:
55+
interface: certificate_transfer
56+
limit: 1
57+
```
58+
59+
Initialise the tracing thing (FIXME) with the names of these relations.
60+
61+
```py
62+
import ops.tracing
63+
64+
class SomeCharm(ops.CharmBase):
65+
def __init__(self, framework: ops.Framework):
66+
...
67+
self._tracing = ops.tracing.Tracing(
68+
tracing_relation_name="charm-tracing",
69+
ca_relation_name="send-ca-cart",
70+
)
71+
```
72+
"""
73+
super().__init__(charm, f'{tracing_relation_name}+{ca_relation_name}')
74+
self.charm = charm
75+
self.tracing_relation_name = tracing_relation_name
76+
self.ca_relation_name = ca_relation_name
77+
self.ca_data = ca_data
78+
79+
# FIXME: Pietro recommends inspecting charm meta to validate the interface name
80+
assert self.charm.meta.relations[tracing_relation_name].interface_name == 'tracing'
81+
self._tracing = TracingEndpointRequirer(
82+
self.charm,
83+
tracing_relation_name,
84+
protocols=['otlp_http'],
85+
)
86+
87+
# TODO: COS requirer
88+
89+
for event in (
90+
# FIXME: validate this
91+
# - the start event may happen after relation-joined?
92+
# - the k8s container died and got restarted
93+
# - the vm [smth] crashed and got restarted
94+
self.charm.on.start,
95+
# In case the previous charm version has a relation but didn't save the URL
96+
self.charm.on.upgrade_charm,
97+
self._tracing.on.endpoint_changed,
98+
self._tracing.on.endpoint_removed,
99+
):
100+
self.framework.observe(event, self._reconcile)
101+
102+
self._certificate_transfer = None
103+
if ca_relation_name:
104+
self._certificate_transfer = CertificateTransferRequires(charm, ca_relation_name)
105+
assert (
106+
self.charm.meta.relations[ca_relation_name].interface_name
107+
== 'certificate_transfer'
108+
)
109+
for event in (
110+
self._certificate_transfer.on.certificate_set_updated,
111+
self._certificate_transfer.on.certificates_removed,
112+
):
113+
self.framework.observe(event, self._reconcile)
114+
115+
def _reconcile(self, _event: Any):
116+
url, ca = self._get_config()
117+
self.charm.set_tracing_destination(url=url, ca=ca)
118+
119+
def _get_config(self) -> tuple[str | None, str | None]:
120+
if not self._tracing.is_ready():
121+
return None, None
122+
123+
url = self._tracing.get_endpoint('otlp_http')
124+
125+
if not url or not url.startswith(('http://', 'https://')):
126+
return None, None
127+
128+
if url.startswith('http://'):
129+
return url, None
130+
131+
if not self._certificate_transfer:
132+
return url, self.ca_data
133+
134+
ca_rel = self.model.get_relation(self.ca_relation_name) if self.ca_relation_name else None
135+
ca_rel_id = ca_rel.id if ca_rel else None
136+
137+
if ca_rel and self._certificate_transfer.is_ready(ca_rel):
138+
return url, '\n'.join(
139+
sorted(self._certificate_transfer.get_all_certificates(ca_rel_id))
140+
)
141+
else:
142+
return None, None

ops/_tracing/readme.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FIXME
2+
3+
relation names
4+
5+
example
6+
7+
How do I series?
8+
- system ca list
9+
- certifi ca list
10+
- cert provider
11+
- integration testing

0 commit comments

Comments
 (0)