Skip to content

Commit 89ca86e

Browse files
committed
feat: ops[tracing]
1 parent 0951842 commit 89ca86e

27 files changed

+1432
-44
lines changed

docs/requirements.txt

+60-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is autogenerated by pip-compile with Python 3.11
33
# by the following command:
44
#
5-
# pip-compile --extra=docs --output-file=docs/requirements.txt pyproject.toml
5+
# pip-compile --extra=docs,tracing --output-file=docs/requirements.txt pyproject.toml
66
#
77
alabaster==1.0.0
88
# via sphinx
@@ -29,6 +29,11 @@ click==8.1.8
2929
# via uvicorn
3030
colorama==0.4.6
3131
# via sphinx-autobuild
32+
deprecated==1.2.15
33+
# via
34+
# opentelemetry-api
35+
# opentelemetry-exporter-otlp-proto-http
36+
# opentelemetry-semantic-conventions
3237
docutils==0.21.2
3338
# via
3439
# canonical-sphinx-extensions
@@ -41,6 +46,8 @@ gitdb==4.0.12
4146
# via gitpython
4247
gitpython==3.1.44
4348
# via canonical-sphinx-extensions
49+
googleapis-common-protos==1.66.0
50+
# via opentelemetry-exporter-otlp-proto-http
4451
h11==0.14.0
4552
# via uvicorn
4653
html5lib==1.1
@@ -51,6 +58,10 @@ idna==3.10
5158
# requests
5259
imagesize==1.4.1
5360
# via sphinx
61+
importlib-metadata==8.5.0
62+
# via
63+
# opentelemetry-api
64+
# ops (pyproject.toml)
5465
jinja2==3.1.5
5566
# via
5667
# myst-parser
@@ -73,8 +84,44 @@ mdurl==0.1.2
7384
# via markdown-it-py
7485
myst-parser==4.0.0
7586
# via ops (pyproject.toml)
87+
opentelemetry-api==1.29.0
88+
# via
89+
# opentelemetry-exporter-otlp-proto-http
90+
# opentelemetry-instrumentation
91+
# opentelemetry-instrumentation-urllib
92+
# opentelemetry-sdk
93+
# opentelemetry-semantic-conventions
94+
# ops (pyproject.toml)
95+
opentelemetry-exporter-otlp-proto-common==1.29.0
96+
# via opentelemetry-exporter-otlp-proto-http
97+
opentelemetry-exporter-otlp-proto-http==1.29.0
98+
# via ops (pyproject.toml)
99+
opentelemetry-instrumentation==0.50b0
100+
# via
101+
# opentelemetry-instrumentation-urllib
102+
opentelemetry-instrumentation-urllib==0.50b0
103+
# via ops (pyproject.toml)
104+
opentelemetry-proto==1.29.0
105+
# via
106+
# opentelemetry-exporter-otlp-proto-common
107+
# opentelemetry-exporter-otlp-proto-http
108+
opentelemetry-sdk==1.29.0
109+
# via opentelemetry-exporter-otlp-proto-http
110+
opentelemetry-semantic-conventions==0.50b0
111+
# via
112+
# opentelemetry-instrumentation
113+
# opentelemetry-instrumentation-urllib
114+
# opentelemetry-sdk
115+
opentelemetry-util-http==0.50b0
116+
# via opentelemetry-instrumentation-urllib
76117
packaging==24.2
77-
# via sphinx
118+
# via
119+
# opentelemetry-instrumentation
120+
# sphinx
121+
protobuf==5.29.3
122+
# via
123+
# googleapis-common-protos
124+
# opentelemetry-proto
78125
pygments==2.19.1
79126
# via
80127
# furo
@@ -90,6 +137,7 @@ pyyaml==6.0.2
90137
requests==2.32.3
91138
# via
92139
# canonical-sphinx-extensions
140+
# opentelemetry-exporter-otlp-proto-http
93141
# sphinx
94142
six==1.17.0
95143
# via html5lib
@@ -148,7 +196,9 @@ sphinxext-opengraph==0.9.1
148196
starlette==0.45.2
149197
# via sphinx-autobuild
150198
typing-extensions==4.12.2
151-
# via anyio
199+
# via
200+
# anyio
201+
# opentelemetry-sdk
152202
uc-micro-py==1.0.3
153203
# via linkify-it-py
154204
urllib3==2.3.0
@@ -165,4 +215,11 @@ websocket-client==1.8.0
165215
# via ops (pyproject.toml)
166216
websockets==14.1
167217
# via sphinx-autobuild
218+
wrapt==1.17.2
219+
# via
220+
# deprecated
221+
# opentelemetry-instrumentation
222+
# opentelemetry-instrumentation-dbapi
223+
zipp==3.21.0
224+
# via importlib-metadata
168225
./testing/

dont-merge/fake-charm.py

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env python
2+
# Copyright 2025 Canonical Ltd.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""FIXME dummy_load docstring."""
16+
17+
from __future__ import annotations
18+
19+
import time
20+
21+
import opentelemetry.trace
22+
23+
import ops
24+
25+
tracer = opentelemetry.trace.get_tracer(__name__)
26+
27+
28+
class DatabaseReadyEvent(ops.charm.EventBase):
29+
"""Event representing that the database is ready."""
30+
31+
32+
class DatabaseRequirerEvents(ops.framework.ObjectEvents):
33+
"""Container for Database Requirer events."""
34+
35+
ready = ops.charm.EventSource(DatabaseReadyEvent)
36+
37+
38+
class DatabaseRequirer(ops.framework.Object):
39+
"""Dummy docstring."""
40+
41+
on = DatabaseRequirerEvents() # type: ignore
42+
43+
def __init__(self, charm: ops.CharmBase):
44+
"""Dummy docstring."""
45+
super().__init__(charm, 'foo')
46+
self.framework.observe(charm.on.start, self._on_db_changed)
47+
48+
def _on_db_changed(self, event: ops.StartEvent) -> None:
49+
"""Dummy docstring."""
50+
self.on.ready.emit()
51+
52+
53+
class FakeCharm(ops.CharmBase):
54+
"""Dummy docstring."""
55+
56+
def __init__(self, framework: ops.Framework):
57+
"""Dummy docstring."""
58+
super().__init__(framework)
59+
self.framework.observe(self.on.setup_tracing, self._on_setup_tracing)
60+
self.framework.observe(self.on.start, self._on_start)
61+
self.framework.observe(self.on.collect_app_status, self._on_collect_app_status)
62+
self.db_requirer = DatabaseRequirer(self)
63+
self.framework.observe(self.db_requirer.on.ready, self._on_db_ready)
64+
65+
def _on_setup_tracing(self, event: ops.SetupTracingEvent) -> None:
66+
"""Dummy docstring."""
67+
self.dummy_load(event)
68+
event.set_destination(url='http://localhost:4318/v1/traces')
69+
70+
def _on_start(self, event: ops.StartEvent) -> None:
71+
"""Dummy docstring."""
72+
self.dummy_load(event)
73+
event.defer()
74+
75+
def _on_db_ready(self, event: DatabaseReadyEvent) -> None:
76+
self.dummy_load(event)
77+
78+
def _on_collect_app_status(self, event: ops.CollectStatusEvent) -> None:
79+
"""Dummy docstring."""
80+
self.dummy_load(event)
81+
event.add_status(ops.ActiveStatus('app seems ready'))
82+
83+
def _on_collect_unit_status(self, event: ops.CollectStatusEvent) -> None:
84+
"""Dummy docstring."""
85+
self.dummy_load(event)
86+
event.add_status(ops.ActiveStatus('unit ready'))
87+
88+
@tracer.start_as_current_span('FakeCharm.dummy_load')
89+
def dummy_load(self, event: ops.EventBase, duration: float = 0.0001) -> None:
90+
"""Dummy docstring."""
91+
print(event)
92+
time.sleep(duration)
93+
94+
95+
if __name__ == '__main__':
96+
ops.main(FakeCharm)

dont-merge/metadata.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name: testmetest

dont-merge/otel-collector-config.yaml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
receivers:
2+
otlp:
3+
protocols:
4+
grpc:
5+
endpoint: "[::]:4317"
6+
http:
7+
endpoint: "[::]:4318"
8+
9+
processors:
10+
batch:
11+
12+
exporters:
13+
debug:
14+
verbosity: detailed
15+
jaeger:
16+
endpoint: jaeger:14250
17+
tls:
18+
insecure: true
19+
20+
service:
21+
pipelines:
22+
traces:
23+
receivers: [otlp]
24+
processors: [batch]
25+
exporters: [debug]

dont-merge/readme.md

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
### Usage
2+
3+
Recommended for traces of moderate and high complexity:
4+
5+
```command
6+
dima@colima-ahh /c/operator (feat-otel)> docker run --rm --name jaeger \
7+
-p 16686:16686 \
8+
-p 4317:4317 \
9+
-p 4318:4318 \
10+
-p 5778:5778 \
11+
-p 9411:9411 \
12+
jaegertracing/jaeger:2.2.0
13+
```
14+
15+
After which, you should be able to:
16+
- open http://192.168.107.4:16686/ in your browser
17+
- select the correct **Service** (`testapp-charm` at current branch state)
18+
- click Search at the bottom of the form
19+
20+
Note: the `jaeger` container keeps traces in memory, and your Service can't be selected
21+
until it has sent some data to `jaeger`.
22+
23+
Alternatively, text-based:
24+
25+
```command
26+
dima@colima-ahh /c/operator (feat-otel)> docker run -it --rm \
27+
-v (pwd)/dont-merge/otel-collector-config.yaml:/etc/otel-collector-config.yaml \
28+
-p 4317:4317 \
29+
-p 4318:4318 \
30+
otel/opentelemetry-collector:latest \
31+
--config=/etc/otel-collector-config.yaml
32+
```
33+
34+
and then
35+
36+
```command
37+
dima@colima-ahh /c/operator (feat-otel)> uv venv --seed .ahh-venv
38+
Using CPython 3.13.0
39+
Creating virtual environment with seed packages at: .ahh-venv
40+
41+
dima@colima-ahh /c/operator (feat-otel)> . .ahh-venv/bin/activate.fish
42+
(.ahh-venv) dima@colima-ahh /c/operator (feat-otel)>
43+
44+
(.ahh-venv) dima@colima-ahh /c/operator (feat-otel)> uv pip install -e .[tracing] -U
45+
Using Python 3.13.0 environment at .ahh-venv
46+
Resolved 21 packages in 907ms
47+
Prepared 18 packages in 72ms
48+
...
49+
50+
(.ahh-venv) dima@colima-ahh /c/operator (feat-otel)> python dont-merge/send-traces.py
51+
Span created and exported to the collector!
52+
```
53+
54+
### Hacking
55+
56+
Or, trying to run code outside of a charm.
57+
58+
Somehow I'm not getting anything, because the `juju-log` hook tool is missing.
59+
60+
Let's fix that.
61+
62+
```command
63+
> ln -s (which echo) juju-log
64+
```
65+
66+
Generate some tracing data:
67+
68+
```command
69+
(venv) > JUJU_UNIT_NAME=testapp/42 JUJU_CHARM_DIR=dont-merge/ PATH=$PATH:. JUJU_VERSION=3.5.4 ./dont-merge/start
70+
```
71+
72+
OTEL collector debug output would look like this:
73+
74+
```
75+
2025-01-15T08:46:23.229Z info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
76+
2025-01-15T08:46:23.229Z info ResourceSpans #0
77+
Resource SchemaURL:
78+
Resource attributes:
79+
-> telemetry.sdk.language: Str(python)
80+
-> telemetry.sdk.name: Str(opentelemetry)
81+
-> telemetry.sdk.version: Str(1.29.0)
82+
-> service.name: Str(testapp-charm)
83+
-> compose_service: Str(testapp-charm)
84+
-> charm_type: Str(CharmBase)
85+
-> juju_unit: Str(testapp/42)
86+
-> juju_application: Str(testapp)
87+
-> juju_model: Str()
88+
-> juju_model_uuid: Str()
89+
ScopeSpans #0
90+
ScopeSpans SchemaURL:
91+
InstrumentationScope ops
92+
Span #0
93+
Trace ID : 8c3f292c89f29c59f1b37fe59ba0abbc
94+
Parent ID :
95+
ID : e0253a03ef694a4f
96+
Name : ops.main
97+
Kind : Internal
98+
Start time : 2025-01-15 08:46:23.175916835 +0000 UTC
99+
End time : 2025-01-15 08:46:23.182329655 +0000 UTC
100+
Status code : Error
101+
Status message : RuntimeError: command not found: is-leader
102+
Events:
103+
SpanEvent #0
104+
-> Name: exception
105+
-> Timestamp: 2025-01-15 08:46:23.182316071 +0000 UTC
106+
-> DroppedAttributesCount: 0
107+
-> Attributes::
108+
-> exception.type: Str(RuntimeError)
109+
-> exception.message: Str(command not found: is-leader)
110+
-> exception.stacktrace: Str(Traceback (most recent call last):
111+
...
112+
-> exception.escaped: Str(False)
113+
{"kind": "exporter", "data_type": "traces", "name": "debug"}
114+
```

0 commit comments

Comments
 (0)