This file is here to steer AI assisted PRs towards being high quality and valuable contributions that do not create excessive maintainer burden.
Monorepo with OpenTelemetry instrumentation packages for Generative AI client libraries, frameworks
and the shared opentelemetry-util-genai utilities.
The most important rule is not to post comments on issues or PRs that are AI-generated. Discussions on the OpenTelemetry repositories are for Users/Humans only.
Follow the PR scoping guidance in CONTRIBUTING.md. Keep AI-assisted PRs tightly isolated to the requested change and never include unrelated cleanup or opportunistic improvements unless they are strictly necessary for correctness.
- One logical change per PR. Do not bundle multiple fixes, refactors, or features into the same PR - split them up so each can be reviewed and reverted independently.
- Run the linter and the relevant tests locally and make sure they pass. See Commands.
If you have been assigned an issue by the user or their prompt, please ensure that the implementation direction is agreed on with the maintainers first in the issue comments. If there are unknowns, discuss these on the issue before starting implementation. Do not forget that you cannot comment for users on issue or pull request threads on their behalf as it is against the rules of this project.
Keep description short and focus on what is being changed and any gaps or concerns.
AI-generated analyses, long reports, or design dumps go in a relevant issue or a separate PR comment - not in the PR description.
instrumentation/- GenAI instrumentation packagesutil/opentelemetry-util-genai/- shared GenAI utilitiesutil/opentelemetry-test-util-genai/- shared test fixtures and assertion helpers (workspace-internal, not published)
Instrumentation packages live under src/opentelemetry/instrumentation/genai/{name}/ with their
own pyproject.toml and tests/. The util package follows the equivalent layout under
src/opentelemetry/util/genai/.
- Instrumentation packages are named
opentelemetry-instrumentation-genai-{lib}and import asopentelemetry.instrumentation.genai.{lib}— e.g.opentelemetry-instrumentation-genai-anthropicimportsopentelemetry.instrumentation.genai.anthropic. - Packages use the OpenTelemetry beta versioning format
MAJOR.MINORbN(e.g.1.0b0).version.pycarries a.devsuffix during development (1.0b0.dev); the release workflow drops it.
- Use compatible release specifiers (e.g.,
~= x.yor>= x.y.z, < (x+1)) that pin the major version and allow minor/patch updates. - Avoid pinning versions to exact patch ranges (like
== x.y.zor~= x.y.z) inpyproject.tomlunless strictly necessary. - Keep version requirements for shared OpenTelemetry packages (e.g.,
opentelemetry-api,opentelemetry-instrumentation, andopentelemetry-semantic-conventions) consistent across all packages. - For OpenTelemetry-owned beta/pre-release packages (e.g.,
opentelemetry-instrumentation,opentelemetry-semantic-conventions,opentelemetry-util-genai), use>=specifiers and pin the upper boundary to the next major version (e.g.,>= 0.64b0, <1for0.xpackages, or>= 1.0b0, <2for1.xpackages) rather than using~=.
A new package under instrumentation/<pkg>/ (where <pkg> is the full
opentelemetry-instrumentation-genai-<lib> directory name) wires in as follows.
Copy the shape from an existing package — paths in tox.ini are repo-root-relative.
- uv workspace: auto-included via the
instrumentation/*glob in rootpyproject.toml [tool.uv.workspace] members— no edit needed. tox.ini:envlist: addpy3{…}-test-instrumentation-genai-<lib>-{oldest,latest}, thepy3{…}-…-<lib>-conformanceentry, andlint-instrumentation-genai-<lib>.[testenv] deps: add the factor-conditional test-requirements lines (<lib>-{oldest,latest,conformance}: -r …/tests/requirements.<factor>.txtplus{[testenv]test_deps}/{[testenv]pytest_deps}). Requirements install here — not incommands_pre.[testenv] commands: add the pytest line (it--ignorestests/test_conformance.py), the separate…-conformancepytest line, andlint-…: sh -c "cd instrumentation && ruff check <pkg>".[testenv:typecheck] deps: add{toxinidir}/instrumentation/<pkg>[instruments].
[tool.pyright](in rootpyproject.toml):includeis opt-in and added to progressively as a package gets fully typed. When a package is ininclude, also add its<pkg>/tests/**/*.pyand<pkg>/examples/**/*.pytoexclude— tests and examples stay untyped;src/**is never excluded.
# Install all packages and dev tools
uv sync --frozen --all-packages
# All pre-commit hooks (ruff, ruff-format, uv-lock, rstcheck) — the CI lint gate
uv run tox -e precommit
# …or just the ruff hook while iterating
uv run pre-commit run ruff --all-files
# Test one package (append -oldest / -latest for the version-matrix variants)
uv run tox -e py312-test-instrumentation-genai-openai-latest
# Run a package's conformance scenarios (only *-conformance envs collect test_conformance.py)
uv run tox -e py314-test-instrumentation-genai-openai-conformance
# Type check (pyright)
uv run tox -e typecheckBefore opening a PR, run uv run tox -e precommit, uv run tox -e typecheck, and the changed package's
test envs (-oldest and -latest, plus -conformance if it ships scenarios) — these mirror
the CI gates.
tox reuses cached envs and won't re-resolve dependencies on its own, so pass --recreate (-r)
after editing a tests/requirements.*.txt or a pyproject.toml dependency bound — otherwise the
run silently uses the previously installed versions.
- Each package has its own
pyproject.tomlwith version, dependencies, and entry points. - The monorepo uses
uvworkspaces. tox.inidefines the test matrix - check it for available test environments.- Do not add
type: ignorecomments. If a type error arises, solve it properly or write a follow-up plan to address it in another PR. - Annotate function signatures (parameters and return types) and class attributes. Prefer
from __future__ import annotationsover runtime-quoted strings. - When a file uses
from __future__ import annotations, do not quote type annotations just to avoid forward references. Keep quotes only for expressions still evaluated at runtime, such astyping.cast(...), unless the referenced type is imported at runtime. - Whenever applicable, all code changes should have tests that actually validate the changes.
This repo uses towncrier to manage changelogs.
- Do not edit
CHANGELOG.mddirectly — thechangelogworkflow rejects PRs that do. - For changes with user-visible impact, add a fragment at
<package>/.changelog/<PR_NUMBER>.<type>containing a one-line description. Types:added,changed,deprecated,removed,fixed. - Don't include the PR number in the body — towncrier appends it from the filename.
- Preview locally with
uv run tox -e changelog-preview.
Apply to packages under instrumentation/.
- Spans, logs, metrics, and events should go through
opentelemetry-util-genai. Do not call OTelTracer/Meter/Loggerdirectly, and import only its public surface — never anopentelemetry.util.genai._*module. - Content capture, hooks, and configuration are owned by the util. Don't add instrumentation-local env vars or settings.
A streamed response only finishes once the caller has drained the stream, so the invocation must
stay open until then. Do not call invocation.stop() when the SDK returns the stream — the
span would close before any chunks arrive.
Instrument streams by subclassing SyncStreamWrapper / AsyncStreamWrapper from
opentelemetry.util.genai.stream (the public, supported helpers). The base class proxies the
underlying SDK stream, drives iteration, and finalizes telemetry exactly once on success, error,
or close(). Subclasses pass the SDK stream to super().__init__(stream) and implement three
hooks:
_process_chunk(chunk)— accumulate per-chunk state (e.g. response model, finish reasons, token usage, streamed content) onto the invocation._on_stream_end()— finalize on success; set the accumulated response attributes and callinvocation.stop()._on_stream_error(error)— finalize on failure; callinvocation.fail(error).
class MyStreamWrapper(SyncStreamWrapper[Chunk]):
def __init__(self, stream, invocation, capture_content):
super().__init__(stream)
self._self_invocation = invocation
...
def _process_chunk(self, chunk): ... # accumulate state
def _on_stream_end(self): self._self_invocation.stop()
def _on_stream_error(self, error): self._self_invocation.fail(error)The hooks are called internally by the wrapper lifecycle.
Instance state must use the wrapt-proxy _self_-prefixed attribute convention (e.g.
self._self_invocation) so it isn't forwarded to the wrapped stream. Don't reimplement iteration,
finalization, or error handling in instrumentations — extend the wrapper instead, and if a hook
isn't enough, add the capability here rather than working around it.
- When catching exceptions from the underlying library to record telemetry, always re-raise the original exception unmodified.
- Do not raise new exceptions in instrumentation/telemetry code.
- Use the semconv attribute and metrics modules under
opentelemetry.semconv— do not hardcode attribute or metric name strings. - For attributes with a well-known value set, use the generated enum from the same module instead of string literals.
- Each package's
README.rstis published as its PyPI long description. When a change introduces user-visible changes to the public API, configuration (env vars,instrument()keyword arguments), supported operations/span types, or examples, update the packageREADME.rstin the same PR so its claims stay accurate. README.rstmust render on PyPI. Do not use Sphinx-only roles (e.g.:class:,:mod:); they fail the PyPI renderer. Runuv run tox -e readmeto validate.
- For every public API instrumented, cover sync/async variants when both exist.
- Cover happy path and error scenarios.
- For streamed responses, cover two exception paths — a stream-side error raised by the SDK
mid-iteration (e.g. an injected
ConnectionError) and a caller-side error raised inside thewith …stream(…) as stream:block before the stream is drained. Assert both re-raise unchanged and still finalize the span with the matchingerror.type. - Tests must verify exact attribute names and value types, checked against the semconv spec.
- Test against oldest and latest supported library versions via
tests/requirements.{oldest,latest}.txtand{oldest,latest}tox.inifactors. - The
oldestenv must install exactly the lower bounds declared inpyproject.toml(dependenciesand theinstrumentsextra) — the declared and tested versions must not drift.UV_RESOLUTION=lowest-direct(set on theoldestfactor) derives them frompyproject.toml, so it stays the single source of truth; only pin test-only deps with nopyproject.tomlbound intests/requirements.oldest.txt. tests/conftest.pymust consume the shared fixtures fromopentelemetry.test_util_genaiby registering them as plugins. Always register the fixtures plugin; register the VCR plugin too when the package's tests use VCR cassettes —pytest_plugins = ["opentelemetry.test_util_genai.fixtures", "opentelemetry.test_util_genai.vcr"](drop thevcrentry for packages with no cassette-backed tests) — rather than re-implementing provider/exporter/VCR plumbing. Import scrub helpers (scrub_response_headers/scrub_response_headers_overwrite) fromopentelemetry.test_util_genai.vcrwhere avcr_configneeds them.- Drive instrumentation in tests through the shared
instrumentcontext manager fromopentelemetry.test_util_genai.instrumentor—instrument(SomeInstrumentor(), tracer_provider=…, logger_provider=…, meter_provider=…, semconv=…, content_capture=…). It sets the content-capture (OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT) env var before instrumenting and restores them after, so a package'sinstrument_*fixtures don't manage that env themselves (TelemetryHandlersnapshots content-capture at construction, so the env must be set before it is built). - When recording VCR cassettes, scrub account-identifying values in the conftest's
vcr_config(filter_headersfor requests,scrub_response_headers_overwritefor responses) before committing. Examples:authorization,openai-organization,openai-project,Set-Cookie, and any response-body field tied to a real account. - An AI-synthesized cassette (recorded without provider access) must start with a
# TODO: this is generated by AI, re-recordcomment so it gets re-recorded against the real provider later.
Packages with substantive instrumentation ship tests/conformance/<scenario>.py
scenarios and a tests/test_conformance.py that validates emitted telemetry
against the GenAI semantic conventions
via Weaver live-check. Each scenario module defines a subclass of
opentelemetry.test_util_genai.conformance.Scenario that sets
expected_spans, expected_metrics, and implements
run(*, tracer_provider, meter_provider, logger_provider, vcr).
Ship a scenario for every semconv operation the library emits, even an operation currently blocked by a util-genai or semconv gap. Skipping the scenario hides the gap; writing it records the gap (as a declared violation or a skip reason) so it fails loudly once the gap is fixed. Never drop a scenario file because it would fail today.
Run via uv run tox -e py314-test-instrumentation-genai-<lib>-conformance. The
*-conformance tox envs target tests/test_conformance.py directly; the
regular *-{oldest,latest} envs --ignore it so they don't need the
OTLP/gRPC exporter or weaver_live_check.
The parallel PR-review rules live in
.github/instructions/instrumentation.instructions.md
and should be kept in sync with this section.
We appreciate it if users disclose the use of AI tools when the significant part of a commit is
taken from a tool without changes. When making a commit this should be disclosed through an
Assisted-by: commit message trailer.
Examples:
Assisted-by: ChatGPT 5.2
Assisted-by: Claude Opus 4.6