Skip to content

Migrate tracing interface lib #581

Description

@dwilding

We have a draft PR in progress (#439) but prefer to restart this work. I asked an agent to follow the migration guide, troubleshoot any issues, and write a clean step-by-step plan. I carefully reviewed and revised the plan (with the agent's help) and have included the final version below.


This plan is based on https://canonical.com/juju/docs/charmlibs/how-to/migrate/.

Source

Pre-existing state

interfaces/tracing/ already exists with only interface/{v0,v1,v2}/ and a ruff.toml. No pyproject.toml, no library code. The ruff.toml has quote-style = "preserve" and schema.py ignores that must be folded into the new pyproject.toml.


Commit 1: Scaffold

mv interfaces/tracing /tmp/tracing_interface_backup
just init --interface --no-input project_slug=tracing author="The Observability team at Canonical"
cp -r /tmp/tracing_interface_backup/interface interfaces/tracing/

Read the pre-existing ruff.toml (now at /tmp/tracing_interface_backup/ruff.toml) and incorporate its settings into pyproject.toml:

  • [tool.ruff.format]quote-style = "preserve"
  • [tool.ruff.lint.extend-per-file-ignores]"./**/schema.py" = ["CPY", "D", "E501"]

The template already generates a [tool.pyright] section with include = ["src", "tests"], so no pyright changes are needed.

The old ruff.toml is not copied back from the backup, so there is nothing to delete — the settings now live only in pyproject.toml.

Prune unused template files (consistent with otlp and tls-certificates which have none of these):

rm -rf interfaces/tracing/docs
rm -rf interfaces/tracing/testing
rm -rf interfaces/tracing/tests/functional
rm -rf interfaces/tracing/tests/integration
rm interfaces/tracing/tests/unit/test_version.py
rm interfaces/tracing/tests/unit/test_version_in_charm.py

Clean up pyproject.toml — the template generates references to the testing subpackage that was just pruned. Remove all three:

  1. Delete the [project.optional-dependencies] section (contains testing = ["charmlibs-interfaces-tracing-testing==0.0.0.dev0"]).
  2. Delete the [tool.uv.sources] section (contains charmlibs-interfaces-tracing-testing = { path = "testing", editable = true }).
  3. In the unit dependency group, replace "charmlibs-interfaces-tracing[testing]" with "pytest".

Commit: scaffold interface library


Commit 2: Verbatim copy

curl -sL "https://github.com/canonical/tempo-operators/raw/refs/heads/main/coordinator/lib/charms/tempo_coordinator_k8s/v0/tracing.py" \
    -o interfaces/tracing/src/charmlibs/interfaces/tracing/_tracing.py

No changes. Gives reviewers a clean baseline.

Note: Linting and type checking are not expected to pass on this commit — the verbatim Charmhub file uses legacy typing imports and has long lines. These are addressed in commits 5, 6, 7, and 8.

Commit: copy tracing.py verbatim from Charmhub


Commit 3: Adapt imports and packaging

_tracing.py

  1. Replace copyright header with Apache 2.0 license header (copy from __init__.py).
  2. Move the large ## Overview module docstring to __init__.py (see below), then replace the module docstring in _tracing.py with: """Private implementation of the :mod:charmlibs.interfaces.tracing package. Migrated from the Charmhub-hosted ``charms.tempo_coordinator_k8s.v0.tracing`` library (LIBAPI 0, LIBPATCH 11)."""
  3. Delete LIBID, LIBAPI, LIBPATCH, PYDEPS.

__init__.py

  1. Replace the template docstring ("""The charmlibs.interfaces.tracing package.""") with the large ## Overview docstring moved from _tracing.py. Update import examples in the docstring from from charms.tempo_coordinator_k8s.v0.tracing import ... to from charmlibs.interfaces.tracing import .... Keep the # noqa: W505 comment at the end of the docstring (it will be removed by ruff in commit 7).
  2. Import public API from ._tracing, add to __all__. Public API: TracingEndpointProvider, TracingEndpointRequirer, TracingEndpointProviderEvents, TracingEndpointRequirerEvents, charm_tracing_config, receiver_protocol_to_transport_protocol, ReceiverProtocol, RawReceiver, Receiver, ProtocolType, TransportProtocolType, DatabagModel, TracingProviderAppData, TracingRequirerAppData, AmbiguousRelationUsageError, BrokenEvent, DataAccessPermissionError, DataValidationError, EndpointChangedEvent, EndpointRemovedEvent, NotReadyError, ProtocolNotRequestedError, RelationInterfaceMismatchError, RelationNotFoundError, RelationRoleMismatchError, RequestEvent, TracingError. (Do not export DEFAULT_RELATION_NAME or RELATION_INTERFACE_NAME — they are internal constants.)

pyproject.toml

just add interfaces/tracing 'ops>=2.23.1,<4' 'pydantic>=2'

Verify

just unit interfaces/tracing

This will report "no tests collected" (exit code 5) — the template's test files were pruned in commit 1 and the requirer test is migrated in commit 9. The command still verifies that the package imports correctly (coverage runs the import).

Note: Linting and type checking are not expected to pass on this commit — the migrated code still uses legacy typing imports and has long lines. These are addressed in commits 5, 6, 7, and 8. Do not add # noqa comments or other workarounds to silence lint errors at this stage.

Commit: adapt imports and packaging for charmlibs


Commit 4: Docstring fixes (required for docs build)

The original Charmhub library uses Markdown formatting in its docstrings (Markdown headers, single-backtick inline code). The charmlibs reference docs are built with Sphinx, which uses reStructuredText (RST). Convert all docstrings to RST format so they render correctly. See the certificate_transfer interface library for an example of the expected RST format.

__init__.py docstring

The Overview docstring (moved here in commit 3) needs the following RST conversions:

  1. Summary line. Replace ## Overview. with a one-line summary: Provide and consume tracing data using the ``tracing`` interface.
  2. Section headers. Convert Markdown ## Requirer Library Usage and ## Provider Library Usage to RST section headers with === underlines (matching the certificate_transfer pattern).
  3. Inline code. Convert all single backticks `code` to double backticks code for proper RST literal rendering. Single backticks render as <cite> (interpreted text) in Sphinx, not as code.
  4. No \* escape needed. With double backticks, *protocol in request_protocols(*protocol:str, ...) renders literally — no \* escape is needed (and no r""" prefix is needed in commit 7).
  5. Bullet list. Add a blank line before the - ``otlp_grpc``` bullet list so RST renders it as a
      ` instead of a paragraph.
    • Numbered list. Add : after "two things" and indent continuation lines to align with the list item text, so RST renders it as an <ol> instead of a paragraph.
    • Remove leading spaces from continuation lines. Three lines have a spurious leading space ( is exposed by the Tempo charm, This relation must use, The TracingEndpointRequirer object). These cause Sphinx to interpret them as block quotes rather than continuation of the paragraph.
    • Add :: to two "as follows" lines. Change as follows to as follows:: so that the following code blocks are rendered as RST literal blocks.

get_endpoint docstring (_tracing.py)

  • Fix Raises: section to Google style. In the original source, ProtocolNotRequestedError: is at the same indentation level as Raises: (not indented under it), and the description is on the next line. Napoleon expects the exception type indented under Raises: with the description on the same line. Also reflow the long description line to stay under 99 chars.
  • Convert `is_ready` to is_ready (double backticks for RST literal rendering).

charm_tracing_config docstring (_tracing.py)

  • Update the stale charm_tracing_config import path in the Usage: example from from lib.charms.tempo_coordinator_k8s.v0.tracing import charm_tracing_config to from charmlibs.interfaces.tracing import charm_tracing_config. (The trace_charm import on the line above is deliberately left pointing at the Charmhub library — see reviewer note ci: add github workflow to run tests #2.)
  • Convert the If/Else block from single-space-indented definition list items to a proper RST bullet list (- item), so it renders as <ul> with <li> items.
  • Change Usage: to Usage:: and indent the code block, so it renders as a syntax-highlighted <pre> block instead of escaped text in a <p>.

Verify

just docs html interfaces/tracing

Note: Linting and type checking are not expected to pass on this commit. Only the docs build needs to pass.

Commit: fix docstrings for Sphinx build


Commit 5: Reflow long lines in _tracing.py and __init__.py

Manually reflow all lines over 99 chars in both _tracing.py and __init__.py. These are comments, docstring text, string literals, and # type: ignore lines that ruff format can't auto-fix. Commit 4 already reflowed one long line in the get_endpoint docstring.

In _tracing.py, there are 11 such lines. In __init__.py, there are 4 long lines in the Overview docstring (moved there in commit 3). Reflowing these now — rather than leaving them for commit 7 — keeps all manual reflow of existing long lines in one commit.

After reflowing, verify that no long lines remain:

awk 'length > 99' interfaces/tracing/src/charmlibs/interfaces/tracing/_tracing.py interfaces/tracing/src/charmlibs/interfaces/tracing/__init__.py

Note: Linting and type checking are not expected to pass on this commit — only the long-line reflow is done here. The remaining lint errors (legacy typing imports, etc.) are addressed in commits 6 and 7, and type checking is addressed in commit 8.

Commit: reflow long lines in _tracing.py and __init__.py


Commit 6: Format

This commit runs ruff format only — pure formatting (whitespace, line wrapping, indentation, trailing commas). No semantic changes. Linting is not expected to pass yet — that is addressed in commit 7.

uv run --only-group=fast-lint ruff format interfaces/tracing

Expected changes

Ruff's formatter will:

  • Wrap long lines and adjust whitespace throughout _tracing.py.
  • Adjust indentation and trailing commas as needed.

These are purely cosmetic changes — no type annotations, import reordering, docstring reflow, or other lint fixes. Those are handled by ruff check --fix in commit 7.

Verify

just docs html interfaces/tracing

Commit: format with ruff


Commit 7: Lint and format

This is the first commit where linting is expected to pass. Commits 2–6 deliberately leave lint errors unresolved. Type checking is not expected to pass yet — that is addressed in commit 8.

just format interfaces/tracing

Expected type annotation modernization

just format runs ruff format (which was already done in commit 6, so is a no-op here) and then ruff check --fix, which will automatically modernize all type annotations in _tracing.py via ruff's UP006, UP007, and UP035 rules. This is expected and correct — the changes are idiomatic Python 3.10+ syntax. The full set of changes:

Before After
typing.Dict dict
typing.List list
typing.Tuple tuple
typing.Optional[X] X | None
typing.Union[X, Y] X | Y
from typing import MutableMapping, Sequence from collections.abc import MutableMapping, Sequence
"...".format(args) f"...{args}" (via UP032)

Note: UP032 converts .format() calls to f-strings, which can create new long lines (f-strings can't be auto-split across lines by the formatter). After ruff check --fix runs, check for and manually reflow any new E501 violations introduced by f-string conversion, using implicit string concatenation (e.g., f"part one " f"part two").

Additionally, ruff check --fix will:

  • Remove # noqa: D101 comments (no longer needed after docstrings are in place).
  • Reorder imports in __init__.py alphabetically (via I001 / isort).
  • Remove the # noqa: W505 from __init__.py (via RUF100). This # noqa: W505 was carried over from the original Charmhub source (which had it on the module docstring) when the docstring was moved to __init__.py in commit 3. It's no longer needed after the docstring was reflowed in commit 5.

Note: Commit 4 uses double backticks for inline code (RST format), so there is no \* escape in the docstring. This means W605 does not fire and r""" is not needed — the docstring remains a regular """ string.

These changes mean commit 7's diff includes semantic changes to type annotations, not just whitespace. The modernized annotations are what pyright will see in commit 8.

Ruff per-file ignores (pyproject.toml)

"src/charmlibs/interfaces/tracing/_tracing.py" = [
    "B904",  # FIXME: use raise ... from
    "PERF203",  # FIXME: try-except within loop
    "RUF012",  # FIXME: mutable class attribute
    "SIM102",  # FIXME: nested if
]
"tests/**/*" = [
    "E501",  # line too long
]

Verify

just fast-lint interfaces/tracing
just docs html interfaces/tracing

Commit: lint and format


Commit 8: Configure type checking

This is the first commit where type checking is expected to pass. The type annotations in _tracing.py will have been modernized by just format in commit 7 (see the "Expected type annotation modernization" section there).

Pyright ignores (pyproject.toml)

ignore = [
    "src/charmlibs/interfaces/tracing/_tracing.py",  # FIXME: ~200 errors
]

The ~200 pyright errors in _tracing.py are pre-existing (the original library was never strict-type-checked). The type annotation modernization in commit 7 doesn't fix or introduce pyright errors — it just changes their nature.

Verify

just static interfaces/tracing

Commit: configure type checking


Commit 9: Migrate unit tests

Migrate test_tracing_requirer.py only. The provider, legacy, and ingressed tests depend on TempoCoordinatorCharm and are not migrated in this PR.

The requirer test already has a minimal MyCharm(CharmBase) and just needs imports updated. It defines its own context fixture locally and does not depend on the source conftest.py (which is for TempoCoordinatorCharm tests).

conftest.py: The template generates a tests/unit/conftest.py with only a license header and docstring (no fixtures). Keep it as-is — the requirer test defines its own context fixture inline.

Unit test dependencies: The template's unit dependency group already includes ops[testing] (which provides ops.testing.Context, Relation, State — the replacement for scenario). No additional dependencies are needed: pytest is provided repo-wide by test-requirements.txt, and the tempo import (used only for Tempo.receiver_ports) is removed by replacing with hardcoded port numbers.

Adapt:

  1. Imports: from charms.tempo_coordinator_k8s.v0.tracing import ...from charmlibs.interfaces.tracing import ...
  2. Test framework: from scenario import ...from ops.testing import ... (Context, Relation, State)
  3. Rename tracing = Relation(...) to tracing_rel (avoids shadowing module name)
  4. Replace Tempo.receiver_ports with hardcoded port numbers (4317, 4318, 9411)
  5. Remove from tempo import Tempo (charm-specific module, not available in charmlibs)
  6. Add type annotations to all test parameters (repo uses typeCheckingMode = "strict"): context: Context[MyCharm], leader: bool, etc. For tuple-unpacked variables that are unused, prefix with _ — but only if the variable is genuinely not referenced afterward (ruff's RUF052 will flag a _-prefixed variable that is accessed).
  7. # type: ignore[operator] on .dump() call. The TracingRequirerAppData.dump() method (inherited from DatabagModel) has no return type annotation, so pyright can't verify the == comparison. Suppress with # type: ignore[operator].
  8. list[tuple[ReceiverProtocol, int]] annotation on local variable. A local variable needs an explicit type annotation for pyright to accept it under strict mode.
  9. ReceiverProtocol import. Needed for the type annotation above — add it to the import from charmlibs.interfaces.tracing.

Verify

just unit interfaces/tracing
just static interfaces/tracing

Commit: migrate requirer unit tests


Commit 10: Update library metadata

Edit .docs/reference/libs.yaml:

  1. Add charmlibs.interfaces.tracing entry (following the tls_certificates pattern):
    - name: charmlibs.interfaces.tracing
      status: recommended
      url: https://pypi.org/project/charmlibs-interfaces-tracing
      docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/tracing
      src: https://github.com/canonical/charmlibs/tree/main/interfaces/tracing
      kind: PyPI
      rel_name: tracing
      rel_url_charmhub: https://charmhub.io/integrations/tracing
      rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tracing/v2
      description: Provide and consume tracing data.
      tags:
      - observability
  2. Mark charms.tempo_coordinator_k8s.tracing as status: legacy and update description to: Provide and consume tracing data. Deprecated in favor of ``charmlibs.interfaces.tracing``.
  3. Update charms.tempo_k8s.tracing description from Deprecated in favour of ``tempo_coordinator_k8s .tracing``. to: Deprecated in favor of ``charmlibs.interfaces.tracing``. This fixes both the deprecation target (now points to the charmlibs package) and a typo in the original (the spurious space in tempo_coordinator_k8s .tracing).

Commit: update library metadata for tracing migration


Commit 11: CHANGELOG and version bump (before release)

Replace the empty CHANGELOG.md (generated by the template) with:

# 1.0.0

Initial release, migrated from charms.tempo_coordinator_k8s.v0.tracing (LIBAPI 0, LIBPATCH 11).

Bump _version.py from 0.0.0.dev0 to 1.0.0. CI blocks non-dev versions without a CHANGELOG entry.

Verify

just check interfaces/tracing

Commit: add changelog and bump version to 1.0.0

Notes for reviewers

  1. Pydantic v1/v2 dual code paths. The if int(pydantic.version.VERSION.split(".")[0]) < 2: branches are dead code (we depend on pydantic>=2). PR #439 takes a different approach: it removes the v1 branches entirely and requires pydantic~=2.0. We can decide which approach to take through PR discussion.

  2. charm_tracing_config docstring references a deprecated, unmigrated library. The Usage: example imports trace_charm from lib.charms.tempo_coordinator_k8s.v0.charm_tracing — a separate, deprecated Charmhub library (not part of this migration). Commit 4 updates the charm_tracing_config import path but leaves the trace_charm import pointing at the Charmhub library. PR #439 takes the same approach and adds a deprecation comment above the function (# the path forward should be charmlibs.xx.tracing and ops[tracing].). We can decide whether to add a similar comment through PR discussion.

  3. Only requirer test migrated. The provider, legacy, and ingressed tests depend on TempoCoordinatorCharm and are not migrated in this PR. PR #439 additionally migrates the provider test by writing a minimal MyCharm(ops.CharmBase) wrapping TracingEndpointProvider to replace the TempoCoordinatorCharm dependency. We can decide whether to adopt this approach through PR discussion.

  4. ~200 pyright errors in _tracing.py suppressed with a FIXME ignore. Original library was never strict-type-checked.

  5. DatabagModel is exported as public API. The plan exports all pydantic models (including DatabagModel, ProtocolType, Receiver, TracingProviderAppData, TracingRequirerAppData, TransportProtocolType) in __all__. DatabagModel in particular is a base class that consumers could subclass to construct relation data directly, bypassing the provider/requirer objects. Is this intentional, or should DatabagModel (and possibly the other models) be treated as internal?

  6. No Discourse docs to migrate. The migration guide includes a step to import tutorials/how-to guides from Discourse. The tempo-coordinator Charmhub page has no substantial Discourse-hosted documentation, so this step is N/A.

  7. Should the v2 interface.yaml lib field be updated? The lib field in interfaces/tracing/interface/v2/interface.yaml still references charms.tempo_coordinator_k8s.tracing (the old Charmhub library name). It was not updated in this migration because the Charmhub library still exists. Should it be updated to charmlibs.interfaces.tracing, or left as-is until the Charmhub library is removed?

Metadata

Metadata

Assignees

Labels

charmlibs-interfaces-tracingRelated to the tracing interface specifically.team-charm-techMaintained by the canonical/charm-tech team.team-tracing-and-profilingMaintained by the canonical/tracing-and-profiling team.

Type

Fields

No fields configured for Task.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions