Skip to content

Commit 8df1151

Browse files
[DPE-9443] Otel distribution cleanup (#1532)
* Lock file maintenance * Otel distributions cleanup * Factor out helper and unit tests * Downgrade amd runners to jammy * Only run on Juju 2 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1 parent 6cbfaf9 commit 8df1151

6 files changed

Lines changed: 464 additions & 349 deletions

File tree

poetry.lock

Lines changed: 342 additions & 339 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires-poetry = ">=2.0.0"
77

88
[tool.poetry.dependencies]
99
python = "^3.10"
10-
ops = {extras = ["tracing"], version = "^3.6.0"}
10+
ops = {extras = ["tracing"], version = "^3.7.0"}
1111
boto3 = "^1.42.74"
1212
pgconnstr = "^1.0.1"
1313
requests = "^2.32.5"
@@ -90,11 +90,6 @@ log_cli_level = "INFO"
9090
asyncio_mode = "auto"
9191
markers = ["juju2", "juju3", "juju_secrets"]
9292

93-
# Formatting tools configuration
94-
[tool.black]
95-
line-length = 99
96-
target-version = ["py38"]
97-
9893
# Linting tools configuration
9994
[tool.ruff]
10095
# preview and explicit preview are enabled for CPY001

spread.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ backends:
4343
CONCIERGE_EXTRA_SNAPS: charmcraft
4444
CONCIERGE_EXTRA_DEBS: pipx
4545
systems:
46-
- ubuntu-24.04:
46+
- ubuntu-22.04:
4747
username: runner
4848
prepare: |
4949
systemctl disable --now unattended-upgrades.service
@@ -80,6 +80,9 @@ backends:
8080
8181
echo "runner:$SPREAD_PASSWORD" | sudo chpasswd
8282
83+
# Remove the preinstalled lxd
84+
sudo snap remove --purge --terminate lxd
85+
8386
ADDRESS localhost
8487
8588
# HACK: spread does not pass environment variables set on runner
@@ -94,7 +97,7 @@ backends:
9497
LANDSCAPE_ACCOUNT_NAME: '$(HOST: echo $LANDSCAPE_ACCOUNT_NAME)'
9598
LANDSCAPE_REGISTRATION_KEY: '$(HOST: echo $LANDSCAPE_REGISTRATION_KEY)'
9699
systems:
97-
- ubuntu-24.04:
100+
- ubuntu-22.04:
98101
username: runner
99102
- ubuntu-24.04-arm:
100103
username: runner

src/charm.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@
1919
from typing import Literal, get_args
2020
from urllib.parse import urlparse
2121

22+
from utils import _remove_stale_otel_sdk_packages
23+
24+
# apply hacky patch to remove stale opentelemetry sdk packages on upgrade-charm.
25+
# it could be trouble if someone ever decides to implement their own tracer parallel to
26+
# ours and before the charm has inited. We assume they won't.
27+
# !!IMPORTANT!! keep all otlp imports UNDER this call.
28+
_remove_stale_otel_sdk_packages()
29+
30+
# ruff: disable[E402]
2231
import psycopg2
2332
from charmlibs import snap
2433
from charms.data_platform_libs.v0.data_interfaces import DataPeerData, DataPeerUnitData
@@ -122,6 +131,8 @@
122131
from upgrade import PostgreSQLUpgrade, get_postgresql_dependencies_model
123132
from utils import new_password, snap_refreshed
124133

134+
# ruff: enable[E402]
135+
125136
logger = logging.getLogger(__name__)
126137
logging.getLogger("httpx").setLevel(logging.WARNING)
127138
logging.getLogger("httpcore").setLevel(logging.WARNING)

src/utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33

44
"""A collection of utility functions that are used in the charm."""
55

6+
import logging
7+
import os
68
import platform
79
import secrets
10+
import shutil
811
import string
12+
from collections import defaultdict
13+
14+
from importlib_metadata import distributions
915

1016
from constants import (
1117
POSTGRESQL_SNAP_NAME,
@@ -47,3 +53,46 @@ def label2name(label: str) -> str:
4753
The converted name.
4854
"""
4955
return label.rsplit("-", 1)[0] + "/" + label.rsplit("-", 1)[1]
56+
57+
58+
def _remove_stale_otel_sdk_packages():
59+
"""Hack to remove stale opentelemetry sdk packages from the charm's python venv.
60+
61+
See https://github.com/canonical/grafana-agent-operator/issues/146 and
62+
https://bugs.launchpad.net/juju/+bug/2058335 for more context. This patch can be removed after
63+
this juju issue is resolved and sufficient time has passed to expect most users of this library
64+
have migrated to the patched version of juju. When this patch is removed, un-ignore rule E402 for this file in the pyproject.toml (see setting
65+
[tool.ruff.lint.per-file-ignores] in pyproject.toml).
66+
67+
This only has an effect if executed on an upgrade-charm event.
68+
"""
69+
# all imports are local to keep this function standalone, side-effect-free, and easy to revert later
70+
71+
major_version = int(juju_ver.split(".")[0]) if (juju_ver := os.getenv("JUJU_VERSION")) else 3
72+
if os.getenv("JUJU_DISPATCH_PATH") != "hooks/upgrade-charm" or major_version > 2:
73+
return
74+
75+
otel_logger = logging.getLogger("charm_tracing_otel_patcher")
76+
otel_logger.debug("Applying _remove_stale_otel_sdk_packages patch on charm upgrade")
77+
# group by name all distributions starting with "opentelemetry_"
78+
otel_distributions = defaultdict(list)
79+
for distribution in distributions():
80+
name = distribution._normalized_name
81+
if name.startswith("opentelemetry_"):
82+
otel_distributions[name].append(distribution)
83+
84+
otel_logger.debug(f"Found {len(otel_distributions)} opentelemetry distributions")
85+
86+
# If we have multiple distributions with the same name, remove any that have 0 associated files
87+
for name, distributions_ in otel_distributions.items():
88+
if len(distributions_) <= 1:
89+
continue
90+
91+
otel_logger.debug(f"Package {name} has multiple ({len(distributions_)}) distributions.")
92+
for distribution in distributions_:
93+
if not distribution.files: # Not None or empty list
94+
path = distribution._path
95+
otel_logger.info(f"Removing empty distribution of {name} at {path}.")
96+
shutil.rmtree(path)
97+
98+
otel_logger.debug("Successfully applied _remove_stale_otel_sdk_packages patch.")

tests/unit/test_utils.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
# See LICENSE file for licensing details.
33

44
import re
5-
from unittest.mock import patch
5+
from unittest.mock import Mock, patch, sentinel
66

77
from constants import POSTGRESQL_SNAP_NAME
8-
from utils import new_password, snap_refreshed
8+
from utils import _remove_stale_otel_sdk_packages, new_password, snap_refreshed
99

1010

1111
def test_new_password():
@@ -34,3 +34,57 @@ def test_snap_refreshed():
3434
):
3535
assert snap_refreshed("100") is False
3636
assert snap_refreshed("200") is False
37+
38+
39+
def test_remove_stale_otel_sdk_packages():
40+
with (
41+
patch("utils.os.getenv", return_value=None) as _getenv,
42+
patch("utils.shutil") as _shutil,
43+
patch("utils.distributions") as _distributions,
44+
):
45+
other_dist = Mock()
46+
other_dist._normalized_name = "test"
47+
otel_dist = Mock()
48+
otel_dist._normalized_name = "opentelemetry_test"
49+
stale_otel_dist = Mock()
50+
stale_otel_dist._normalized_name = "opentelemetry_test"
51+
stale_otel_dist.files = []
52+
stale_otel_dist._path = sentinel.path
53+
54+
# Not called if not upgrade hook
55+
_remove_stale_otel_sdk_packages()
56+
57+
_distributions.assert_not_called()
58+
_shutil.rmtree.assert_not_called()
59+
_distributions.reset_mock()
60+
_shutil.rmtree.reset_mock()
61+
62+
# don't execute on Juju 3
63+
_getenv.side_effect = ["3.0.0", "hooks/upgrade-charm"]
64+
_remove_stale_otel_sdk_packages()
65+
66+
_distributions.assert_not_called()
67+
_shutil.rmtree.assert_not_called()
68+
_distributions.reset_mock()
69+
_shutil.rmtree.reset_mock()
70+
71+
# Upgrade hook, nothing to remove
72+
_getenv.side_effect = ["2.9.53", "hooks/upgrade-charm"]
73+
_distributions.return_value = [other_dist, otel_dist]
74+
75+
_remove_stale_otel_sdk_packages()
76+
77+
_distributions.assert_called_once_with()
78+
_shutil.rmtree.assert_not_called()
79+
_distributions.reset_mock()
80+
_shutil.rmtree.reset_mock()
81+
82+
# Upgrade hook, duplicate otel packages
83+
_getenv.side_effect = ["2.9.53", "hooks/upgrade-charm"]
84+
_distributions.return_value = [other_dist, otel_dist, stale_otel_dist]
85+
_remove_stale_otel_sdk_packages()
86+
87+
_distributions.assert_called_once_with()
88+
_shutil.rmtree.assert_called_once_with(sentinel.path)
89+
_distributions.reset_mock()
90+
_shutil.rmtree.reset_mock()

0 commit comments

Comments
 (0)