Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ops[tracing] [draftey draft] #1527

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
72 changes: 69 additions & 3 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --extra=docs --output-file=docs/requirements.txt pyproject.toml
# pip-compile --extra=docs,tracing --output-file=docs/requirements.txt pyproject.toml
#
alabaster==1.0.0
# via sphinx
Expand All @@ -29,6 +29,11 @@ click==8.1.8
# via uvicorn
colorama==0.4.6
# via sphinx-autobuild
deprecated==1.2.15
# via
# opentelemetry-api
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-semantic-conventions
docutils==0.21.2
# via
# canonical-sphinx-extensions
Expand All @@ -41,6 +46,8 @@ gitdb==4.0.12
# via gitpython
gitpython==3.1.44
# via canonical-sphinx-extensions
googleapis-common-protos==1.66.0
# via opentelemetry-exporter-otlp-proto-http
h11==0.14.0
# via uvicorn
html5lib==1.1
Expand All @@ -51,6 +58,10 @@ idna==3.10
# requests
imagesize==1.4.1
# via sphinx
importlib-metadata==8.5.0
# via
# opentelemetry-api
# ops (pyproject.toml)
jinja2==3.1.5
# via
# myst-parser
Expand All @@ -73,8 +84,53 @@ mdurl==0.1.2
# via markdown-it-py
myst-parser==4.0.0
# via ops (pyproject.toml)
opentelemetry-api==1.29.0
# via
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-instrumentation
# opentelemetry-instrumentation-dbapi
# opentelemetry-instrumentation-sqlite3
# opentelemetry-instrumentation-urllib
# opentelemetry-sdk
# opentelemetry-semantic-conventions
# ops (pyproject.toml)
opentelemetry-exporter-otlp-proto-common==1.29.0
# via opentelemetry-exporter-otlp-proto-http
opentelemetry-exporter-otlp-proto-http==1.29.0
# via ops (pyproject.toml)
opentelemetry-instrumentation==0.50b0
# via
# opentelemetry-instrumentation-dbapi
# opentelemetry-instrumentation-sqlite3
# opentelemetry-instrumentation-urllib
opentelemetry-instrumentation-dbapi==0.50b0
# via opentelemetry-instrumentation-sqlite3
opentelemetry-instrumentation-sqlite3==0.50b0
# via ops (pyproject.toml)
opentelemetry-instrumentation-urllib==0.50b0
# via ops (pyproject.toml)
opentelemetry-proto==1.29.0
# via
# opentelemetry-exporter-otlp-proto-common
# opentelemetry-exporter-otlp-proto-http
opentelemetry-sdk==1.29.0
# via opentelemetry-exporter-otlp-proto-http
opentelemetry-semantic-conventions==0.50b0
# via
# opentelemetry-instrumentation
# opentelemetry-instrumentation-dbapi
# opentelemetry-instrumentation-urllib
# opentelemetry-sdk
opentelemetry-util-http==0.50b0
# via opentelemetry-instrumentation-urllib
packaging==24.2
# via sphinx
# via
# opentelemetry-instrumentation
# sphinx
protobuf==5.29.3
# via
# googleapis-common-protos
# opentelemetry-proto
pygments==2.19.1
# via
# furo
Expand All @@ -90,6 +146,7 @@ pyyaml==6.0.2
requests==2.32.3
# via
# canonical-sphinx-extensions
# opentelemetry-exporter-otlp-proto-http
# sphinx
six==1.17.0
# via html5lib
Expand Down Expand Up @@ -148,7 +205,9 @@ sphinxext-opengraph==0.9.1
starlette==0.45.2
# via sphinx-autobuild
typing-extensions==4.12.2
# via anyio
# via
# anyio
# opentelemetry-sdk
uc-micro-py==1.0.3
# via linkify-it-py
urllib3==2.3.0
Expand All @@ -165,4 +224,11 @@ websocket-client==1.8.0
# via ops (pyproject.toml)
websockets==14.1
# via sphinx-autobuild
wrapt==1.17.2
# via
# deprecated
# opentelemetry-instrumentation
# opentelemetry-instrumentation-dbapi
zipp==3.21.0
# via importlib-metadata
./testing/
90 changes: 90 additions & 0 deletions dont-merge/fake-charm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python
# Copyright 2025 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""FIXME dummy_load docstring."""

from __future__ import annotations

import time

import opentelemetry.trace

import ops

tracer = opentelemetry.trace.get_tracer(__name__)


class DatabaseReadyEvent(ops.charm.EventBase):
"""Event representing that the database is ready."""


class DatabaseRequirerEvents(ops.framework.ObjectEvents):
"""Container for Database Requirer events."""

ready = ops.charm.EventSource(DatabaseReadyEvent)


class DatabaseRequirer(ops.framework.Object):
"""Dummy docstring."""

on = DatabaseRequirerEvents() # type: ignore

def __init__(self, charm: ops.CharmBase):
"""Dummy docstring."""
super().__init__(charm, 'foo')
self.framework.observe(charm.on.start, self._on_db_changed)

def _on_db_changed(self, event: ops.StartEvent) -> None:
"""Dummy docstring."""
self.on.ready.emit()


class FakeCharm(ops.CharmBase):
"""Dummy docstring."""

def __init__(self, framework: ops.Framework):
"""Dummy docstring."""
super().__init__(framework)
self.framework.observe(self.on.start, self._on_start)
self.framework.observe(self.on.collect_app_status, self._on_collect_app_status)
self.db_requirer = DatabaseRequirer(self)
self.framework.observe(self.db_requirer.on.ready, self._on_db_ready)

def _on_start(self, event: ops.StartEvent) -> None:
"""Dummy docstring."""
ops.set_tracing_destination(url='http://localhost:4318/v1/traces')
self.dummy_load(event, 0.0025)

def _on_db_ready(self, event: DatabaseReadyEvent) -> None:
self.dummy_load(event, 0.001)

def _on_collect_app_status(self, event: ops.CollectStatusEvent) -> None:
"""Dummy docstring."""
self.dummy_load(event)
event.add_status(ops.ActiveStatus('app seems ready'))

def _on_collect_unit_status(self, event: ops.CollectStatusEvent) -> None:
"""Dummy docstring."""
self.dummy_load(event)
event.add_status(ops.ActiveStatus('unit ready'))

@tracer.start_as_current_span('FakeCharm.dummy_load') # type: ignore
def dummy_load(self, event: ops.EventBase, duration: float = 0.001) -> None:
"""Dummy docstring."""
print(event)
time.sleep(duration)


if __name__ == '__main__':
ops.main(FakeCharm)
1 change: 1 addition & 0 deletions dont-merge/metadata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: testmetest
25 changes: 25 additions & 0 deletions dont-merge/otel-collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
receivers:
otlp:
protocols:
grpc:
endpoint: "[::]:4317"
http:
endpoint: "[::]:4318"

processors:
batch:

exporters:
debug:
verbosity: detailed
jaeger:
endpoint: jaeger:14250
tls:
insecure: true

service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug]
114 changes: 114 additions & 0 deletions dont-merge/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
### Usage

Recommended for traces of moderate and high complexity:

```command
dima@colima-ahh /c/operator (feat-otel)> docker run --rm --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-p 5778:5778 \
-p 9411:9411 \
jaegertracing/jaeger:2.2.0
```

After which, you should be able to:
- open http://192.168.107.4:16686/ in your browser
- select the correct **Service** (`testapp-charm` at current branch state)
- click Search at the bottom of the form

Note: the `jaeger` container keeps traces in memory, and your Service can't be selected
until it has sent some data to `jaeger`.

Alternatively, text-based:

```command
dima@colima-ahh /c/operator (feat-otel)> docker run -it --rm \
-v (pwd)/dont-merge/otel-collector-config.yaml:/etc/otel-collector-config.yaml \
-p 4317:4317 \
-p 4318:4318 \
otel/opentelemetry-collector:latest \
--config=/etc/otel-collector-config.yaml
```

and then

```command
dima@colima-ahh /c/operator (feat-otel)> uv venv --seed .ahh-venv
Using CPython 3.13.0
Creating virtual environment with seed packages at: .ahh-venv

dima@colima-ahh /c/operator (feat-otel)> . .ahh-venv/bin/activate.fish
(.ahh-venv) dima@colima-ahh /c/operator (feat-otel)>

(.ahh-venv) dima@colima-ahh /c/operator (feat-otel)> uv pip install -e .[tracing] -U
Using Python 3.13.0 environment at .ahh-venv
Resolved 21 packages in 907ms
Prepared 18 packages in 72ms
...

(.ahh-venv) dima@colima-ahh /c/operator (feat-otel)> python dont-merge/send-traces.py
Span created and exported to the collector!
```

### Hacking

Or, trying to run code outside of a charm.

Somehow I'm not getting anything, because the `juju-log` hook tool is missing.

Let's fix that.

```command
> ln -s (which echo) juju-log
```

Generate some tracing data:

```command
(venv) > JUJU_UNIT_NAME=testapp/42 JUJU_CHARM_DIR=dont-merge/ PATH=$PATH:. JUJU_VERSION=3.5.4 ./dont-merge/start
```

OTEL collector debug output would look like this:

```
2025-01-15T08:46:23.229Z info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
2025-01-15T08:46:23.229Z info ResourceSpans #0
Resource SchemaURL:
Resource attributes:
-> telemetry.sdk.language: Str(python)
-> telemetry.sdk.name: Str(opentelemetry)
-> telemetry.sdk.version: Str(1.29.0)
-> service.name: Str(testapp-charm)
-> compose_service: Str(testapp-charm)
-> charm_type: Str(CharmBase)
-> juju_unit: Str(testapp/42)
-> juju_application: Str(testapp)
-> juju_model: Str()
-> juju_model_uuid: Str()
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope ops
Span #0
Trace ID : 8c3f292c89f29c59f1b37fe59ba0abbc
Parent ID :
ID : e0253a03ef694a4f
Name : ops.main
Kind : Internal
Start time : 2025-01-15 08:46:23.175916835 +0000 UTC
End time : 2025-01-15 08:46:23.182329655 +0000 UTC
Status code : Error
Status message : RuntimeError: command not found: is-leader
Events:
SpanEvent #0
-> Name: exception
-> Timestamp: 2025-01-15 08:46:23.182316071 +0000 UTC
-> DroppedAttributesCount: 0
-> Attributes::
-> exception.type: Str(RuntimeError)
-> exception.message: Str(command not found: is-leader)
-> exception.stacktrace: Str(Traceback (most recent call last):
...
-> exception.escaped: Str(False)
{"kind": "exporter", "data_type": "traces", "name": "debug"}
```
Loading
Loading