Skip to content

Commit d7cfe5d

Browse files
committed
feat: ops[tracing]
1 parent a508075 commit d7cfe5d

23 files changed

+1179
-20
lines changed

docs/requirements.txt

+69-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,53 @@ 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-dbapi
92+
# opentelemetry-instrumentation-sqlite3
93+
# opentelemetry-instrumentation-urllib
94+
# opentelemetry-sdk
95+
# opentelemetry-semantic-conventions
96+
# ops (pyproject.toml)
97+
opentelemetry-exporter-otlp-proto-common==1.29.0
98+
# via opentelemetry-exporter-otlp-proto-http
99+
opentelemetry-exporter-otlp-proto-http==1.29.0
100+
# via ops (pyproject.toml)
101+
opentelemetry-instrumentation==0.50b0
102+
# via
103+
# opentelemetry-instrumentation-dbapi
104+
# opentelemetry-instrumentation-sqlite3
105+
# opentelemetry-instrumentation-urllib
106+
opentelemetry-instrumentation-dbapi==0.50b0
107+
# via opentelemetry-instrumentation-sqlite3
108+
opentelemetry-instrumentation-sqlite3==0.50b0
109+
# via ops (pyproject.toml)
110+
opentelemetry-instrumentation-urllib==0.50b0
111+
# via ops (pyproject.toml)
112+
opentelemetry-proto==1.29.0
113+
# via
114+
# opentelemetry-exporter-otlp-proto-common
115+
# opentelemetry-exporter-otlp-proto-http
116+
opentelemetry-sdk==1.29.0
117+
# via opentelemetry-exporter-otlp-proto-http
118+
opentelemetry-semantic-conventions==0.50b0
119+
# via
120+
# opentelemetry-instrumentation
121+
# opentelemetry-instrumentation-dbapi
122+
# opentelemetry-instrumentation-urllib
123+
# opentelemetry-sdk
124+
opentelemetry-util-http==0.50b0
125+
# via opentelemetry-instrumentation-urllib
76126
packaging==24.2
77-
# via sphinx
127+
# via
128+
# opentelemetry-instrumentation
129+
# sphinx
130+
protobuf==5.29.3
131+
# via
132+
# googleapis-common-protos
133+
# opentelemetry-proto
78134
pygments==2.19.1
79135
# via
80136
# furo
@@ -90,6 +146,7 @@ pyyaml==6.0.2
90146
requests==2.32.3
91147
# via
92148
# canonical-sphinx-extensions
149+
# opentelemetry-exporter-otlp-proto-http
93150
# sphinx
94151
six==1.17.0
95152
# via html5lib
@@ -148,7 +205,9 @@ sphinxext-opengraph==0.9.1
148205
starlette==0.45.2
149206
# via sphinx-autobuild
150207
typing-extensions==4.12.2
151-
# via anyio
208+
# via
209+
# anyio
210+
# opentelemetry-sdk
152211
uc-micro-py==1.0.3
153212
# via linkify-it-py
154213
urllib3==2.3.0
@@ -165,4 +224,11 @@ websocket-client==1.8.0
165224
# via ops (pyproject.toml)
166225
websockets==14.1
167226
# via sphinx-autobuild
227+
wrapt==1.17.2
228+
# via
229+
# deprecated
230+
# opentelemetry-instrumentation
231+
# opentelemetry-instrumentation-dbapi
232+
zipp==3.21.0
233+
# via importlib-metadata
168234
./testing/

dont-merge/fake-charm.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 FakeCharm(ops.CharmBase):
29+
"""Dummy docstring."""
30+
31+
def __init__(self, framework: ops.Framework):
32+
"""Dummy docstring."""
33+
super().__init__(framework)
34+
self.framework.observe(self.on.start, self._on_start)
35+
self.framework.observe(self.on.collect_app_status, self._on_collect_app_status)
36+
self.framework.observe(self.on.collect_unit_status, self._on_collect_unit_status)
37+
38+
def _on_start(self, event: ops.StartEvent) -> None:
39+
"""Dummy docstring."""
40+
ops.set_tracing_destination(url='http://localhost:4318/v1/traces')
41+
self.dummy_load(event, 0.0025)
42+
43+
def _on_collect_app_status(self, event: ops.CollectStatusEvent) -> None:
44+
"""Dummy docstring."""
45+
self.dummy_load(event)
46+
event.add_status(ops.ActiveStatus('app seems ready'))
47+
48+
def _on_collect_unit_status(self, event: ops.CollectStatusEvent) -> None:
49+
"""Dummy docstring."""
50+
self.dummy_load(event)
51+
event.add_status(ops.ActiveStatus('unit ready'))
52+
53+
@tracer.start_as_current_span('FakeCharm.dummy_load') # type: ignore
54+
def dummy_load(self, event: ops.EventBase, duration: float = 0.001) -> None:
55+
"""Dummy docstring."""
56+
print(event)
57+
time.sleep(duration)
58+
59+
60+
if __name__ == '__main__':
61+
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+
```

dont-merge/send-traces.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 dummy docstring."""
15+
16+
from __future__ import annotations
17+
18+
import logging
19+
20+
import opentelemetry.trace
21+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
22+
from opentelemetry.sdk.resources import Resource
23+
from opentelemetry.sdk.trace import TracerProvider
24+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
25+
26+
# The default ProxyTracer allows tracers to be declared ahead of time like loggers
27+
logger = logging.getLogger(__name__)
28+
tracer = opentelemetry.trace.get_tracer(__name__)
29+
30+
# 1. Create a tracer provider with a "service.name" resource attribute
31+
opentelemetry.trace.set_tracer_provider(
32+
TracerProvider(resource=Resource.create({'service.name': 'example-service'}))
33+
)
34+
35+
# 2. Configure the OTLP HTTP exporter (defaults to protobuf format)
36+
otlp_exporter = OTLPSpanExporter(
37+
endpoint='http://localhost:4318/v1/traces'
38+
# If you needed headers or auth, you could add them like:
39+
# headers={"Authorization": "Bearer <TOKEN>"},
40+
)
41+
42+
# 3. Create a span processor (BatchSpanProcessor recommended for production)
43+
span_processor = BatchSpanProcessor(otlp_exporter)
44+
opentelemetry.trace.get_tracer_provider().add_span_processor(span_processor) # type: ignore
45+
46+
47+
@tracer.start_as_current_span('some label') # type: ignore
48+
def main(foo: int = 42):
49+
"""Do something."""
50+
# can't add attributes to a decorator, if needed use the below instead
51+
#
52+
# with tracer.start_as_current_span("some label") as span:
53+
# span.set_attribute('foo', 'bar')
54+
# span.add_event('sample_event', {'event_attr': 123})
55+
56+
logger.info('Span created and will be exported to the collector soon!')
57+
58+
59+
if __name__ == '__main__':
60+
logging.basicConfig(level='INFO')
61+
main()
62+
# from typing_extensions import reveal_type
63+
# reveal_type(main)

0 commit comments

Comments
 (0)